____ _   _ ____   ___  __  __ ___ _____ ___  ___
  / ___| | | |  _ \ / _ \|  \/  |_ _| ____/ _ \/ _ \
 | |  _| |_| | |_) | | | | |\/| || |  _|| | | | | | |
 | |_| |  _  |  __/| |_| | |  | || | |__| |_| | |_| |
  \____|_| |_|_|    \___/|_|  |_|___|_____\___/ \___/

community.poke.site - architecture notes

you are here: community.poke.site  >  architecture


Community Poke Architecture

edition 1993 - last updated 24 June 2026


This document is a written tour of the runtime pieces behind a Community Poke project: the Telegram / Discord bridge running on TDLib; the poke-bot that proxies through Discord's bot gateway; the custom MCP servers a project can plug into the agent loop; the trigger system that fires on datetime and cron schedules and on inbound webhooks; and the persistent /workspace/user/ filesystem where every byte that survives a redeploy lives.

It is written for a curious operator who wants to take the whole thing apart in an afternoon and be able to put it back together by dinner. There is no marketing copy below. There are no buzzwords.

Contents

  1. Telegram-Discord Bridge and TDLib
  2. Proxying through the Discord Bot
  3. Custom MCP Connections
  4. Triggers and Automations
  5. Physical Filesystems

System Plumbing (an overview)

The diagram below shows the data path of one inbound message and one outbound response, plus the side channels (MCP servers, triggers, secrets, filesystem) that the orchestrator reads from and writes to. Boxes are processes or persistent stores. Arrows are first-class connections. The orchestrator is the only long-lived peer that the outside world speaks to.


        EXTERNAL SYSTEMS
   +-------------------+          +-------------------+
   |     Telegram      |          |      Discord      |
   |    (MTProto)      |          |   (WS gateway)    |
   +---------+----------+          +----------+---------+
             |                                |
             v                                v
   +-------------------+          +-------------------+
   |       TDLib       |          |     poke-bot      |
   |   (client lib)    |          |  (bearer client)  |
   +---------+----------+          +----------+---------+
             |                                |
             +----------------+----------------+
                              |
                              v
                +---------------------------+
                |       bridge daemon       |
                +-------------+-------------+
                              |
                +---------------------------+      +-----------------------+
                |       orchestrator        |<---->|   /workspace/user/    |
                +-------------+-------------+      +-----------------------+
                              |
       +----------+-----------+--------+--------+----+----+
       |          |           |        |        |         |
       v          v           v        v        v         v
   +--------+  +--------+  +--------+ +------+ +------+ +----------+
   |  sub-  |  |  MCP   |  |  cron  | |  dt  | |ingest| |secrets   |
   | agents |  | servers|  | trigger| |trgr  | |  hook| | (decrypt |
   |(build, |  |(stdio, |  +---+----+ +--+---+ +--+---+ |  gated)  |
   | audit) |  | http)  |      |         |       |       +----+-----+
   +---+----+  +---+----+      |         |       |            |
       |          |            v         v       v            v
       |          v         [ wake events -> orchestrator ]
       |      [tool call]    (cron / datetime / ingest)
       |          |                |
       +-----+----+----------------+
             |
             v
        +-----------+
        |   build   |
        +-----+-----+
              |
              v
        +-----------+         +-------------------+
        |  deploy   | ------> | <slug>.poke.site  |
        +-----------+         +-------------------+

1. Telegram - Discord Bridge and TDLib

A Community Poke project integrates Telegram and Discord over a single signed-in MTProto session and a single Discord bot identity. Both run inside the project container; both are first-class peers in the runtime, not external services.

The MTProto side ships as TDLib, the official C++ client for the Telegram protocol. We talk to TDLib over its internal JSON interface. TDLib takes responsibility for everything Telegram-specific: auth-key derivation and rotation, session persistence, flood-wait calculus, the typed update stream, binary download and upload of media, voice notes, stickers, and reactions. The bridge does not poll Telegram; it subscribes to TDLib's update queue and consumes events as TDLib emits them.

Auth key and session.

The auth key is generated on first connect and persisted at /workspace/user/var/tdlib/auth_key.bin. TDLib rotates the key in place when its own invalidation signal fires; we never copy or rotate the key from outside TDLib. The session blob lives next to the key. Together they are the durable identity of the project on Telegram.

Update pipeline.

TDLib emits typed updates (message, messageEdit, messageDelete, chatMember, and so on). The bridge reads each update off the queue, normalizes it into a thin internal event shape (kind, chat_id, source_msg_id, author, timestamp, text-or-media, reply_target), and hands it to the bot path for the Discord-side write.

Why TDLib, not the Bot API.

Three reasons. We want the raw event stream so we never poll. We want binary fidelity for media (voice notes arrive as the bytes Telegram intended, not as something transmuted along the way). We want presence and reaction updates that the Bot API surfaces only after delays. The trade is operational complexity: we own session durability, reconnects, and flood-wait backoff. We accept the trade.

Abridged bridge config:

# /workspace/user/etc/bridge/config.toml
[mproto]
  auth_key_path = "/workspace/user/var/tdlib/auth_key.bin"
  session_path  = "/workspace/user/var/tdlib/session"
  flood_wait_threshold_ms = 8000

[mapping]
  - { telegram_chat_id = -1001234567890,
      discord_channel_id = "1234567890123456789" }

[dedup]
  lru_capacity = 10000

[queue]
  capacity = 256
  eject   = "oldest"

2. Proxying through the Discord Bot

The bridge alone would see only a single channel (whichever Discord channel we hardcoded the webhook for). To reach a whole Discord server, to send slash commands, and to read member state, we need a Discord bot identity running on the project. That bot is the poke-bot.

The poke-bot is a long-lived Node process that boots from /workspace/user/etc/bot/config.json. The config holds a Discord bot token (project application scoped), a per-server guild ID, and a list of channel mappings: which Telegram chat_id sits on which Discord channel_id. The token is read on demand from a project-decrypt-time secrets dir, not from a baked-in value.

Two transports into Discord.

REST. Outbound. The bridge calls poke-bot over a loopback HTTP endpoint at /bot/out on port 9101. The bot turns each bridge event into a Discord message payload and POSTs it via the REST API to the bound channel.

WebSocket gateway. Inbound. Poke-bot maintains a persistent Gateway connection to Discord and receives every event the bot has access to. It pattern-matches against the project channel mapping and pushes matching messages through /bot/in on port 9102, back to the bridge.

Rate limits.

Discord's REST is rate-limited per route; 429 responses include a retry_after. The bot maintains a per-route token bucket. 429s drain the bucket for the indicated duration and reschedule the message. The bridge sees a queue between itself and poke-bot (capped at 256 messages, oldest-evicting) so a slow Discord never bursts upstream.

Slash commands.

Poke-bot registers three slash commands on every project: /poke status (reports bridge / bot health), /poke pause (silences outbound messages without disconnecting), /poke resume (re-enables). Slash commands flow through Discord's interaction endpoint, are verified locally, and re-enter the bridge over /bot/in as a control-kind event.

Presence.

Poke-bot writes a status block to its pinned system channel every ~30 seconds: bot uptime, bridge daemon last-sync timestamp, queue depth, and the last five trigger fires. Operators watch this channel as their at-a-glance health surface.

Abridged bot config:

# /workspace/user/etc/bot/config.json
{
  "token_ref":      "secrets/bot/discord_token",
  "intents":        ["GuildMessages", "MessageContent"],
  "guild_id":       "987654321098765432",
  "system_channel": "1234567890123456790",
  "control_bind":   "127.0.0.1:9101",
  "events_bind":    "127.0.0.1:9102",
  "slash_commands": ["status", "pause", "resume"]
}

3. Custom MCP Connections

MCP, the Model Context Protocol, is the open spec under which an LLM client can ask an external server for a list of tools and then call those tools over JSON-RPC. Community Poke treats MCP servers as first-class peers in the project. Each project can declare any number of MCP servers, and the orchestrator's subagents are free to call any tool exposed by a declared server.

Manifest.

MCP servers are declared in /workspace/user/etc/mcp/servers.json. Each entry carries:

Lifecycle.

The orchestrator starts an MCP server lazily, on the first tool call routed to that server's name, and keeps the process warm for ttl seconds. On idle timeout the orchestrator kills the process; it rehydrates on the next caller. Persistent servers stay alive for the project's lifetime.

Transports in detail.

stdio. The orchestrator spawns the server as a child process and pipes JSON-RPC over stdin / stdout, one frame per tool call. The child inherits the project's secrets dir, not the host filesystem.

http. The server is reachable at a pinned sse URL. The orchestrator opens a long-poll stream and writes tool calls into it. Used for tools that already speak HTTP / SSE.

browser-bridge. A small in-process shim that lets a tool operate a headless browser tab inside the project container. Used for tools that need DOM, cookies, or interaction.

Security boundary.

A subagent may call only the MCP servers explicitly listed in the project's policy. Ambient calls are forbidden. Each call is logged with input shape, output hash, and the calling subagent's task_id. The audit log lives at /workspace/user/var/mcp/audit.log.

Abridged servers.json:

# /workspace/user/etc/mcp/servers.json
[
  {
    "name": "github",
    "transport": "stdio",
    "command": ["/usr/local/bin/mcp-github", "--read-only"],
    "tools": ["list_issues", "read_issue", "search_prs"],
    "env_fingerprint": ["GITHUB_TOKEN"],
    "ttl": 60
  },
  {
    "name": "render",
    "transport": "http",
    "url": "http://render.local:7711/sse",
    "tools": ["render_markdown", "render_pdf"],
    "ttl": 600
  },
  {
    "name": "browser",
    "transport": "browser-bridge",
    "tools": ["open", "click", "screenshot"],
    "ttl": -1
  }
]

4. Triggers and Automations

Triggers are how the project runs without a human in the loop. Three kinds.

Datetime trigger.

A one-shot at a specific UTC timestamp. Stored as a row (trigger_id, fire_at_unix, action, payload). The orchestrator maintains a min-heap ordered by fire_at; the heap is reaped on every minute tick. Suitable for reminders, release-day posts, single alerts that should fire once and not recur.

Cron trigger.

A recurring expression with a timezone. Stored as a row (trigger_id, cron_expr, tz, action, payload). Evaluated by an in-process cron sidecar that wakes every minute. The sidecar walks the registered cron triggers, fires those whose matcher hits, and emits a wake event into the orchestrator. Suitable for "every weekday 09:00 UTC, post a summary".

Ingest trigger.

A webhook-shaped inbound trigger. A unique URL /ingest/<trigger_id> is published for the trigger. Any HTTP POST to that URL with the project's ingest token in the Authorization header (and an optional signed body) is accepted, validated, and turned into a wake event. Suitable for GitHub pushes, Stripe events, inbound from another service we do not fully control.

Action kinds.

Each trigger carries an action of one of:

Storage.

Trigger manifests live at /workspace/user/var/triggers/<trigger_id>/manifest.json. The orchestrator unwatches a trigger when its file is gone; it picks up new ones on the next five-second poll. Polling is cheap (the dir is rarely more than a few dozen manifests).

Failure handling.

A trigger that fails three consecutive fires is marked CRASHED and surfaces in the operator log. An operator can /poke pause it, edit the manifest, or replan the schedule. The orchestrator never silently discards a crashed trigger.

Abridged trigger manifests:

# /workspace/user/var/triggers/daily_summary/manifest.json
{
  "kind":            "cron",
  "cron_expr":       "0 9 * * 1-5",
  "tz":              "UTC",
  "action":          "spawn_task",
  "payload":         { "brief": "post weekday 9am summary" }
}

# /workspace/user/var/triggers/launch_day/manifest.json
{
  "kind":            "datetime",
  "fire_at_unix":    1783207200,
  "action":          "send_message",
  "payload":         { "text": "launch day - go live" }
}

# /workspace/user/var/triggers/gh_push/manifest.json
{
  "kind":            "ingest",
  "ingest_token_ref":"secrets/ingest/gh_push_token",
  "action":          "spawn_task",
  "payload_from":    "request_body"
}

5. Physical Filesystems

The /workspace/user/ filesystem is the project's persistent spine. It is a Railway volume (NVMe-backed) mounted at /workspace/user/ inside the project container. It survives deploys, container restarts, and re-allocations. It is the canonical working area for a project that runs for months. We provision at 10 GB and allow up to a quarter TB per project before the volume is treated as a pet.

What lives there.

Layering.

Each request spawns a tmpfs overlay (copy-on-write) on top of the persistent layer for write-heavy work in subagents. On exit, the overlay is squashed back into the persistent layer for paths marked durable. Ephemeral scratch (/workspace/user/tmp/) never round-trips, which keeps snapshots small and deploys fast.

Quotas, in three places.

Wiping.

Wiping the volume is explicit and unidirectional. The orchestrator exposes a /workspace/purge command that requires confirmation through the bridge channel. There is no other path that erases the volume. A wipe is irreversible.

Layout sketch:

/workspace/user/
  +- repo/                 project working tree (git checkout)
  +- etc/
  |    +- bridge/
  |    |    +- config.toml
  |    |    +- peers.json
  |    |    +- dedup_lru.bin
  |    +- bot/
  |    |    +- config.json
  |    +- mcp/
  |         +- servers.json
  +- var/
  |    +- tdlib/
  |    |    +- auth_key.bin
  |    |    +- session
  |    +- bot/
  |    |    +- presence.json
  |    +- orchestrator/
  |    |    +- events.log
  |    +- triggers/
  |    |    +- <trigger_id>/manifest.json
  |    +- mcp/
  |         +- audit.log
  +- scratch/              per-task scratch, wiped on finish
  +- tmp/                  per-request scratch, never snapshotted
  +- cache/                capped prefixes, cleaned before snapshots
  +- secrets/              decrypt-gated per-project keys
  +- KEEP                  sentinel; presence = no auto-trim

Maintained by the Community Poke maintainers. Last revised 24 June 2026.