arizukocomponents › route tokens

route tokens

What it is

The route_tokens table plus the two URL prefixes that read from it: /chat/<token>/ for visitor web chat, /hook/<token> for inbound webhooks. One row per token, four columns (token_hash, jid, owner_folder, created_at). The JID prefix tells webd which surface a token belongs to.

Each token is 32 random bytes, base64url-encoded. Stored as sha256(token); raw token returned once at issuance.

Why it exists

Two long-standing needs — “give my site a chat URL” and “let GitHub POST events to the agent” — want the same primitive: a URL bound to a destination JID. Before this table the chat URL was hardcoded as one column on the groups table and webhooks improvised on top of it. Pulling the token out into a first-class row gives every inbound URL a uniform shape, a uniform mint surface, and a uniform revoke step.

The two surfaces

GET  /chat/<token>/            → chat widget (HTML)
POST /chat/<token>/            → message in; SSE reply stream out
POST /hook/<token>             → one inbound at the JID; 204; no reply

Chat tokens carry JIDs prefixed web:; hook tokens carry hook:. Both URL prefixes accept any valid token — the kind is metadata for the agent, not a URL gate. Inbound JID is whatever the token row stores; the surface contract (widget+SSE vs fire-and-forget) is fixed by the URL.

How it fits

browser / curl / GitHub / Linear / ...
        |
        v   /chat/<token>/...   or   /hook/<token>
        |
      proxyd     (public path; per-token rate shield)
        |
      webd       (hash token, look up row, append inbound at row.jid)
        |
      gated      (route + spawn the agent; SSE replies only for /chat/)

Issuance is handled in gated — both the MCP tools (issue_chat_link, issue_webhook) and the REST endpoints (POST /v1/route_tokens/chat, POST /v1/route_tokens/hook) call one internal insertRouteToken writer. webd never writes the table; gated never reads the /chat/ or /hook/ URLs.

Mint authorization

The grants tier of the caller bounds which folders they can mint for. Tier 0 mints for any folder; tier 1 mints for self and descendants; tier 2 mints for self only; tier 3+ cannot mint. The row's owner_folder records the issuer's folder, not the JID target — revocation requires admin on owner_folder.

Rate limits

Per-token in-memory bucket in webd. The bucket ceiling is chosen by JID prefix: web: uses a smaller bucket sized for human typing; hook: uses a larger bucket sized for machine bursts. Body cap is 1 MiB. Excess returns 429.

What it does not do

No body signature verification (no X-Hub-Signature check), no second-factor auth on the URL, no per-IP fingerprinting. The bearer model is intentional — the URL is the credential. Upstream HMAC validation is a skill concern, executed on the agent side from the row's headers field.

No automatic reissue on schedule. No back-channel reply on /hook/ — agents that want to acknowledge a webhook reply on a different channel (Slack, email).

Standalone usage

The table and writer live in gated; the URL handlers live in webd. Neither runs standalone — both need the shared SQLite file and the gated /v1/messages endpoint to deliver inbounds. The unit of standalone use is webd itself; see components/webd.

Go deeper