arizuko

arizuko › reference › Store schema

schema

Every table arizuko keeps is an entity. One row is one item, addressed by a (partition, sort) coordinate, and the rest of the row is string attributes. One DynamoDB-protocol server per instance serves them, named by DYNAMODB_URL, and nothing above the driver knows which server it is.

How the layers stack, from a daemon down to PostgreSQL, is drawn on the store. The coordinate is what a read costs, so each entity's coordinate is argued in the file that writes it. store/entities.go names the set each owner holds.

Four daemons own the entities

An owner is a daemon and the tables it alone creates. On the server a table is named <owner>.<entity>, and each daemon signs with its own access key, so the server’s policy bounds what it reaches. store/owner_reach.go declares which owners each daemon opens, and a test fails when the code opens one the table does not list. A daemon reads another owner’s tables only where this page says so.

ownerdeclared inholds
routdstore/entities.gomessages and their indexes, routing, grants, secrets, scheduling, packages
runedruned/runstorespawns, session log, circuit breaker
authdauthd/authstoresigning keys, accounts, refresh tokens
onbodstore/entities.goonboarding, gates, invites

audit_log is in all four. One declaration lives in audit, every owner creates it, and each serves its own read-only at GET /v1/audit; /dash/audit/ merges those four answers rather than reading anybody's tables.

Beside the four, each channel adapter keeps four small tables under its own name, declared in chanlib/chanstore: outbound_dedup, one item per delivery routd asked it to make; inbound_pending, one per inbound routd has not yet accepted; file_urls, one per attachment behind its /files/<id> proxy; ack_marks, one per chat under the acknowledgement reaction an adapter with no typing indicator puts on a turn’s first message, so a restarted adapter can still take it off. emaid adds two of its own, declared in emaid/emaistore: email_threads, one per conversation, holding the reply address and the Message-ID every reply threads under; email_msg_ids, one per Message-ID seen, naming its thread, which is how an inbound In-Reply-To finds its conversation. Both take the id itself as the partition, because every read is a point read and the index grows with every mail.

routd owns the messages

entityaddressed bycarries
messagesthe chat, then the instant plus the message idsender, content, verb, source, attachments, topic, routed_to, errored, is_observed, turn id. The id is in the key because two messages can share an instant inside one chat.
message_foldersthe folder, then the message's own positiona pointer to the message. Routing is dynamic, so the folder a message reached is written when routed_to is stamped and never recomputed.
message_folder_countsthe folderthat folder's message count, so the usage page reads items instead of partitions
message_activityone constant partition, then the message's positionthe newest messages across every chat — the one read that has no coordinate of its own
message_deliverythe delivery status, then the message's positionattempts. sent is the absence of an item, so the entity holds only what still owes a delivery.
chatsthe chat jidagent_cursor (the dispatch watermark), sticky group and topic
sessionsfolder and topicsession id, parent topic, fork instant, observed_cursor
chat_reply_statejid and topicreply anchor, engagement window, owner and explicit stop time
chat_proactivethe chat jidthe instant that chat last spoke unprompted — the cooldown
system_messagesthe folder, then the instantone queued system event

Ranked search is not a table. routd keeps an in-memory BM25 index over the messages entity, walked from the store at boot and fed by its own writes; see find_messages.

routd owns the configuration resources

These fourteen are the registered resources: each has a REST face, an MCP face, and a row in arizuko dump.

entityaddressed bycarries
groupsthe foldername, product, model, container_config (mounts, timeout), daily cost cap, open flag, observe window
routessequence, target and matchthe rule. The sequence is padded so a read comes back in precedence order; the id an operator deletes by is minted through counters.
route_tokensthe sha256 of the tokenjid, owning folder, context, kind (route delivery bearer or pair identity link). The digest is the key; the raw token is never stored.
web_routesthe path prefixaccess (public / auth / deny / redirect), redirect target, owning folder
proxyd_routesthe pathproxyd's reverse-proxy table. proxyd owns every write and serves it on its own /v1.
aclprincipal, action, scope, params, predicate and effectwho granted it, when, and grant_option. The six key fields are the grant.
acl_membershipchild and parentwho added the edge and when. Roles and jid→sub claims share it.
secretsscope kind, scope id and keythe value, sealed AES-256-GCM under SECRETS_KEY
network_rulesfolder and targetthe per-folder egress allowlist. The folder leads because it is also the purge scope.
scheduled_tasksthe task idowner, chat, prompt, cron, next run, status, context mode
pending_actionsthe call idone tool call held for a human verdict
installed_packagesfolder and name (empty folder = instance-wide)source, revision, the identities the package owns, per-asset hashes
github_sourcesthe source keygitd's webhook delivery secret and the folder it delivers to
gdrive_filesthe Drive file idthe bound Drive file and the folder that its comments go to

routd also owns the runtime state

entityaddressed bycarries
secret_use_logthe instant a tool call resolved onespawn, caller, folder, tool, key, outcome, latency
turn_contextthe turn idfolder, topic, chat, trigger sender, run id, state, the hash of the gateway key that turn was metered with
turn_resultsfolder and turn idsession, status, when it was recorded. Create-if-absent is what makes one report one delivery.
task_run_logsthe instant a task rantask, duration, status, result, error
user_profilesthe person's subthe username their folder is named after, their daily spend cap
group_watchersobserver and sourcenothing — the pair is the fact
pane_sessionsteam, user and threadthe Slack assistant pane's channel and context jid
countersthe name of what it numbersthe count. There is one, routes.id, moved by a compare-and-swap.

runed, authd and onbod own their own entities

entityaddressed bycarries
spawnsone constant partition, then the run idfolder, topic, container, session, kind, state, outcome, exit code, timestamps. A guarded write names one item, which is why the run id is the whole address.
session_logthe folder, then the instant it startedsession id, end, result, error, message count
circuit_breakerthe folder, both halvesconsecutive failures and the last one. Every operation is a point operation on one folder.
signing_keysone partition per daemon, then the kidthe ES256 keypair and its retirement
signing_key_slota single itemwhich kid signs now. One item is what makes two signers unrepresentable.
auth_usersthe subname, created
oauth_identities<provider>:<provider sub>the account that login belongs to
refresh_tokensthe token hashfamily, sub, scope, audience, issue, expiry, use and revocation instants
refresh_familiesthe family idone login lineage
onboardingthe chat jidstatus (awaiting_message → queued / approved / refused), gate, sub, timestamps. No credential: the link is a pair row in route_tokens.
onboarding_gatesthe gatedaily limit, enabled
invitesthe invite reftarget glob, issuer, expiry, max uses, used count
invite_redemptionsthe person, then the instantone unspent subgroup redemption. The point delete is the claim.
audit_logthe instant, at fixed width plus a suffixcategory, action, actor, resource, scope, surface, outcome, duration, turn, folder, instance, request, source ip. The id is the sort key and is the instant, so ordering and the page cursor are one value.

An entity is not a SQL table

Four tables share the word “route” and nothing else. routes sends a message to a folder; web_routes decides who may load a URL path; proxyd_routes sends an HTTP path to a backend daemon; route_tokens holds a bearer that lets a stranger post into one chat. They answer four different questions, so they stay four tables.

Only the owner writes an entity

The owner daemon writes its own tables. A few daemons open routd’s tables with keys of their own, each for one job: onbod and dashd for onboarding and the operator’s admin writes, webd to resolve a route token and read a chat, gitd for a webhook’s sealed secret, slakd for the pane context, and proxyd to read acl on every request, because a login-time snapshot would keep a revoked grant working. dashd opens onbod’s too. Everything else goes through the owner's /v1. A mutation carries its audit_log row in the same batch, so a refused write records nothing and a recorded row happened.