arizuko › reference › JID format
jid
Every chat and every sender is a JID of the form <platform>:<rest>. The first segment of <rest> is a kind discriminator. JIDs are plain strings; routing matches them with path.Match glob semantics across all routing keys.
1. Wire form & grammar
A JID is arizuko’s universal address for who and where — one string that works across Telegram, Discord, or the web, so routing rules never need per-platform logic.
jid := platform ":" rest
platform := lowercase adapter name (everything before the first ":")
rest := opaque platform-owned path (everything after the first ":")
kind := first segment of rest before "/"
The platform/rest split is the only structure core enforces. Two helpers in core/types.go perform it:
core.JidPlatform(jid)— everything before the first:;""when there's no colon or the colon is at position 0.core.JidRoom(jid)— everything after the first:; the whole string when there's no colon.
2. Code shape
A JID is a string. Message.ChatJID and Message.Sender are both string fields on core.Message. There is no parse step, no net/url wrapper, and no separate chat-vs-sender Go type — the gateway never interprets <rest>, it only glob-matches the full string. Adapters own the per-platform shape of <rest> (section 3).
3. Per-platform schemas
Each adapter owns its <rest> shape. From specs/5/S-jid-format.md § "Per-platform schemas" plus the rewriter rules in 0042-typed-jids.sql:
| platform | kind | shape | example |
|---|---|---|---|
telegram | user | telegram:user/<chat_id> | telegram:user/12345 |
group | telegram:group/<chat_id> | telegram:group/67890 | |
discord | <guild_id> | discord:<guild_id>/<channel_id> | discord:5678/9012 |
dm | discord:dm/<channel_id> | discord:dm/54321 | |
user | discord:user/<user_id> | discord:user/u123 | |
whatsapp | — (server suffix) | whatsapp:<id>@<server> | whatsapp:1234@g.us, …@s.whatsapp.net, …@lid |
mastodon | account | mastodon:account/<account_id> | mastodon:account/42 |
status | mastodon:status/<status_id> | mastodon:status/abc | |
reddit | comment | reddit:comment/<id> | reddit:comment/abc |
submission | reddit:submission/<id> | reddit:submission/xyz | |
dm | reddit:dm/<id> | reddit:dm/m123 | |
user | reddit:user/<username> | reddit:user/alice | |
bluesky | user | bluesky:user/<did> (DID's : percent-encoded) | bluesky:user/did%3Aplc%3A123 |
post | bluesky:post/<at_uri> | bluesky:post/at%3A… | |
twitter | user | twitter:user/<user_id> | twitter:user/123 |
tweet | twitter:tweet/<tweet_id> | twitter:tweet/456 | |
dm | twitter:dm/<id> | twitter:dm/789 | |
linkedin | user | linkedin:user/<urn> | linkedin:user/urn123 |
post | linkedin:post/<urn> | linkedin:post/urn456 | |
email | address | email:address/<addr> | email:address/foo@bar.com |
thread | email:thread/<msgid> | email:thread/<Message-ID> | |
web | folder | web:<folder>[/<suffix>] | web:acme/support |
user | web:user/<sub> | web:user/sub-1 | |
hook | hook:<folder>/<source>[/<suffix>] | hook:acme/eng/github | |
slack | — | per adapter; slack: JIDs handled by slakd |
4. Routing & glob matching
The route table's match column is a space-separated list of key=glob predicates. Evaluation lives in router.RouteMatches; keys are extracted from the message via msgField:
| key | extracts | source |
|---|---|---|
platform | JidPlatform(msg.ChatJID) — everything before the first : | core/types.go |
room | JidRoom(msg.ChatJID) — everything after the first : | core/types.go |
chat_jid | msg.ChatJID — full canonical string | |
sender | msg.Sender — full sender JID | |
verb | msg.Verb — "message" default; "join", "edit", … |
Glob match uses Go's path.Match semantics:
*matches any non-/sequence (segments are first-class).?matches exactly one non-/char.[abc]/[a-z]matches a character class.\escapes the next character.
From the RouteMatches contract (router.go:290-324):
| predicate | matches when |
|---|---|
key=<exact> | value equals <exact> |
key=<glob> | value matches glob (*, ?, […]; * doesn't cross /) |
key=* | value is present (non-empty) — bare * silently rejects empty |
key= | value is absent (empty) |
| omit key | unconstrained |
The matcher is Go's path.Match applied directly to the JID string; router.RouteMatches is the only matcher. There is no separate core.MatchJID helper — a JID is a string, so path.Match(pattern, jid) is all a pattern check needs.
5. Examples
# all telegram groups
match='platform=telegram chat_jid=telegram:group/*'
# discord guild 67890, any channel/thread
match='chat_jid=discord:67890/*'
# all Discord DMs
match='chat_jid=discord:dm/*'
# all WhatsApp groups (server suffix discriminates)
match='chat_jid=whatsapp:*@g.us'
# all activity on a Mastodon instance (single account-id digit-or-letter)
match='chat_jid=mastodon:account/*'
# specific Telegram room by signed numeric ID (room=<rest after colon>)
match='platform=telegram room=group/-100123456'
# any non-message verb on any platform (join, edit, delete, …)
match='verb=*'
match='verb=' is the negation: only verb-less (default 'message') rows
6. Globs in grant rules
The same jid=<glob> syntax appears in grant rules (grants reference). The matcher there is grants.matchGlob with isValueDelim stopping * at , or ) — not at /. That's a deliberate difference: grant param globs span the whole JID, route globs are segment-aware.
send(jid=telegram:group/*) # rule: only telegram groups (grants matcher: * spans /)
match='chat_jid=telegram:group/*' # route: only telegram groups (path.Match: * doesn't cross /)
For top-level platform globs both forms agree (telegram:*/* in routes vs telegram:* in grants); both mean "anything telegram." For grant params there's only one trailing segment to match against, so the wider * is harmless.
7. Hard cutover & migration history
Migrations 0042-typed-jids.sql and 0043-typed-jids-tail.sql rewrote every JID-shaped value in the store to the typed form. Affected columns:
messages.chat_jid,messages.sender,messages.reply_to_senderchats.jid(PK)onboarding.jid- (pre-0053:
user_jids.jidand vestigialgrants.jidwere rewritten too, before being dropped by the ACL unification) routes.match—chat_jid=/sender=/room=predicatesscheduled_tasks.chat_jidchat_reply_state.jid
Every UPDATE is guarded by a NOT LIKE on the new shape, so re-running is idempotent. Rules:
- Telegram — signed integer split: positive →
telegram:user/<id>, negative →telegram:group/<|id|>(sign dropped). - Discord — split via
chats.is_group: DMs →discord:dm/<channel>, guild channels →discord:_/<channel>(placeholder). - Mastodon — drop host (single-instance per deployment);
account/<id>for senders/recipients. - Reddit — thing-prefix lookup:
t1_→comment,t2_→user,t3_→submission. - Bluesky — percent-encode the embedded
:indid:plc:<rest>→bluesky:user/did%3Aplc%3A<rest>. - Email — senders rewritten to
email:address/<addr>;chat_jidrewrite deferred untilemaidemits typed form. - LinkedIn —
<urn>→user/<urn>. - WhatsApp + Twitter — already conformant; not touched.
- Web —
web:<folder>[/<suffix>]remains folder-keyed; the URL token is decoupled into theroute_tokenstable (one row per URL, JID per row). - Hook — new top-level platform
hook:<folder>/<source>[/<suffix>]for webhook ingest viaroute_tokens;<folder>is the destination folder,senderon inbound is the<source>segment. - Routes — bare
platform:*globs rewritten toplatform:*/*so segment-awarepath.Matchstill matches anything on that platform.
8. Design discipline
- Kind in the path. The first segment after the colon discriminates resource kind on the wire. The role (chat vs sender) is which message field holds the string —
ChatJIDorSender— not a distinct Go type. - Adapter owns its schema. Core only splits
<platform>:<rest>viaJidPlatform/JidRoom. Each adapter constructs and parses<rest>per its own declared shape. - URI-clean. Adapters percent-encode reserved chars in
<rest>(a Bluesky DID's colons, for example) so the platform/rest split holds. - One URL = one resource. No sign-bit hacks, no
t1_/t2_/t3_prefix soup, no in-band server suffixes for code (WhatsApp keeps@serverbecause its existing shape already encodes kind). - Adding a kind later is one string. A new platform kind earns its place when an adapter, the gateway, or the agent treats it differently from siblings. No system-wide format change, no DB migration of existing rows.
9. Go deeper
core/types.go—JidPlatform,JidRoom, theMessagestruct (stringChatJID/Senderfields)router/router.go—RouteMatches,msgField,ResolveRoutespecs/5/S-jid-format.md— full spec, per-platform schemas, design discipline- grants reference — how
jid=…appears in grant rule params - schema reference — every column that stores a JID