arizuko › reference › Store schema
schema
Every table arizuko keeps is an entity: one item per row, addressed by a (partition, sort) coordinate, with the rest of the row as string attributes. One DynamoDB-protocol server per instance serves them, named by DYNAMODB_URL, and nothing above the driver knows which server it is.
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.
1. Four owners
An owner is a daemon and the tables it alone creates. On a served instance the tables are named <owner>.<entity> and each daemon signs with its own access key, so a policy can bound what it reaches; on a file instance each owner gets one bind-mounted directory. Either way a daemon reads another owner's tables only where this page says so.
| owner | declared in | holds |
|---|---|---|
routd | store/entities.go | messages and their indexes, routing, grants, secrets, scheduling, packages |
runed | runed/runstore | spawns, session log, circuit breaker |
authd | authd/authstore | signing keys, accounts, refresh tokens |
onbod | store/entities.go | onboarding, 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.
2. routd — messages
| entity | addressed by | carries |
|---|---|---|
messages | the chat, then the instant plus the message id | sender, 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_folders | the folder, then the message's own position | a 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_counts | the folder | that folder's message count, so the usage page reads items instead of partitions |
message_activity | one constant partition, then the message's position | the newest messages across every chat — the one read that has no coordinate of its own |
message_delivery | the delivery status, then the message's position | attempts. sent is the absence of an item, so the entity holds only what still owes a delivery. |
chats | the chat jid | agent_cursor (the dispatch watermark), sticky group and topic |
sessions | folder and topic | session id, parent topic, fork instant, observed_cursor |
chat_reply_state | jid and topic | last reply, engagement window and the folder holding it |
chat_proactive | the chat jid | the instant that chat last spoke unprompted — the cooldown |
system_messages | the folder, then the instant | one 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.
3. routd — configuration
These thirteen are the registered resources: each has a REST face, an MCP face, and a row in arizuko export.
| entity | addressed by | carries |
|---|---|---|
groups | the folder | name, product, model, container_config (mounts, timeout), daily cost cap, open flag, observe window |
routes | sequence, target and match | the rule. The sequence is padded so a read comes back in precedence order; the id an operator deletes by is minted through counters. |
route_tokens | the sha256 of the token | jid, owning folder, context, kind (route delivery bearer or pair identity link). The digest is the key; the raw token is never stored. |
web_routes | the path prefix | access (public / auth / deny / redirect), redirect target, owning folder |
proxyd_routes | the path | proxyd's reverse-proxy table. proxyd owns every write and serves it on its own /v1. |
acl | principal, action, scope, params, predicate and effect | nothing else — those six fields are the grant |
acl_membership | child and parent | who added the edge and when. Roles and jid→sub claims share it. |
secrets | scope kind, scope id and key | the value, sealed AES-256-GCM under SECRETS_KEY |
secret_use_log | the instant a tool call resolved one | spawn, caller, folder, tool, key, outcome, latency |
network_rules | folder and target | the per-folder egress allowlist. The folder leads because it is also the purge scope. |
scheduled_tasks | the task id | owner, chat, prompt, cron, next run, status, context mode |
pending_actions | the call id | one tool call held for a human verdict |
installed_packages | folder and name (empty folder = instance-wide) | source, revision, the identities the package owns, per-asset hashes |
github_sources | the source key | gitd's webhook delivery secret and the folder it delivers to |
4. routd — the rest
| entity | addressed by | carries |
|---|---|---|
turn_context | the turn id | folder, topic, chat, trigger sender, run id, state, the hash of the gateway key that turn was metered with |
turn_results | folder and turn id | session, status, when it was recorded. Create-if-absent is what makes one report one delivery. |
task_run_logs | the instant a task ran | task, duration, status, result, error |
user_profiles | the person's sub | the username their folder is named after, their daily spend cap |
group_watchers | observer and source | nothing — the pair is the fact |
pane_sessions | team, user and thread | the Slack assistant pane's channel and context jid |
counters | the name of what it numbers | the count. There is one, routes.id, moved by a compare-and-swap. |
5. runed, authd, onbod
| entity | addressed by | carries |
|---|---|---|
spawns | one constant partition, then the run id | folder, 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_log | the folder, then the instant it started | session id, end, result, error, message count |
circuit_breaker | the folder, both halves | consecutive failures and the last one. Every operation is a point operation on one folder. |
signing_keys | one partition per daemon, then the kid | the ES256 keypair and its retirement |
signing_key_slot | a single item | which kid signs now. One item is what makes two signers unrepresentable. |
auth_users | the sub | name, created |
oauth_identities | <provider>:<provider sub> | the account that login belongs to |
refresh_tokens | the token hash | family, sub, scope, audience, issue, expiry, use and revocation instants |
refresh_families | the family id | one login lineage |
onboarding | the chat jid | status (awaiting_message → queued / approved / refused), gate, sub, timestamps. No credential: the link is a pair row in route_tokens. |
onboarding_gates | the gate | daily limit, enabled |
invites | the invite ref | target glob, issuer, expiry, max uses, used count |
invite_redemptions | the person, then the instant | one unspent subgroup redemption. The point delete is the claim. |
audit_log | the instant, at fixed width plus a suffix | category, 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. |
6. What an entity is not
- No columns. Every value is a string and an absent attribute is the only kind of missing, so a comparison means the same thing on every engine.
- No index but the coordinate. A selective read belongs on the sort key; a filter still reads what it filters.
- No foreign key, so nothing cascades. Deleting a group purges every entity that confers on that folder or routes into it, explicitly, by one containment predicate — and a table in neither list fails a completeness test.
- No autoincrement. An item has no server-minted identity; the one number arizuko hands out,
routes.id, comes from thecountersitem through a compare-and-swap, so two concurrent adds cannot mint the same handle. - No full-text index. Ranked message search is derived state in routd's memory, rebuilt from the
messagesentity at boot.
7. Who writes
The owner daemon writes its own tables. Two readers reach across on purpose: proxyd resolves a route token and reads acl per request, because a login-time snapshot would keep a revoked grant working; dashd reads routd's and onbod's tables for the pages it renders. 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.