Files
hub/docs/architecture/storage/table-reference.md
glm-5.1 2b63cda1c7 Setup repo: migrate architecture specs, code stubs, and tasks from alkhub_ts
Copy architecture docs, ADRs, storage domain specs, research, reviews,
and 56 storage architecture tasks from the alkhub_ts monorepo. Adapt for
standalone @alkdev/hub repo structure (src/ not packages/hub/).

Sanitize all sensitive information:
- Replace private IPs (10.0.0.1) with localhost defaults
- Remove internal server hostnames (dev1, ns528096)
- Replace /workspace/ private paths with npm package references
- Remove hardcoded credentials from examples
- Rewrite infrastructure.md without private network details

Add Deno project scaffolding: deno.json (pinned deps), .gitignore,
AGENTS.md, entry point. Migrate existing code stubs (crypto, config
types, logger) with updated import paths.
2026-05-25 10:56:32 +00:00

21 KiB

status, last_updated
status last_updated
draft 2026-04-23

Storage: Table Schemas

Canonical reference for all Drizzle table definitions, decomposed by domain. For overview, patterns, and setup, see ../README.md. For design decisions (ADRs), see ../../../decisions/. For the account-role-session model, see ../../agent-roles.md.

Table Index

File Tables Domain
identity.md accounts, organizations, organization_members, api_keys, audit_logs Auth, access, multi-tenancy
projects.md projects, workspaces Project/workspace management
sessions.md sessions, messages, parts Agent conversations, AI SDK
spokes.md spokes, operations, operation_registrations Spoke registration, operations
services.md clients, client_secrets External service connections
roles.md roles Behavioral role definitions
coordination.md mappings, detections Coordinator workflows
call-graph.md call_graph_nodes, call_graph_edges Call observability
tasks.md tasks, task_dependencies SDD task management
(planned — Phase 2) (future roles_audit) Role change history (deferred)

Common Columns

All tables share these columns (commonCols):

import { text, timestamp, jsonb } from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";

export const commonCols = {
  id: text("id")
    .primaryKey()
    .$defaultFn(() => crypto.randomUUID()),
  metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
  createdAt: timestamp("created_at", { withTimezone: true })
    .default(sql`now()`)
    .notNull(),
  updatedAt: timestamp("updated_at", { withTimezone: true })
    .default(sql`now()`)
    .notNull()
    .$onUpdate(() => new Date()),
};

Note: commonCols.id uses UUIDv4 (random, non-sortable). For tables requiring chronological ordering by ID, only parts uses sortable IDs (see ADR-003). Messages rely on the composite index (session_id, created_at, id) for ordering.

Note: updatedAt uses Drizzle's $onUpdate (application-level). Direct SQL updates bypass this and must manually SET updated_at = now(). For critical tables, consider a Postgres trigger as a safety net.

Foreign Key Cascade Behavior

Relationship onDelete Rationale
organizations.ownerId → accounts.id RESTRICT Administrative owner — cannot be deleted while org exists. Transfer via org.transferOwnership before account deletion.
organization_members.orgId → organizations.id CASCADE Deleting an org removes all memberships
organization_members.accountId → accounts.id CASCADE Deleting an account removes all memberships
projects.orgId → organizations.id SET NULL Org deletion detaches projects but preserves them
workspaces.projectId → projects.id CASCADE Deleting a project removes all its workspaces
sessions.projectId → projects.id CASCADE Deleting a project removes all its sessions
sessions.workspaceId → workspaces.id SET NULL Workspace deletion detaches sessions but preserves them
sessions.parentId → sessions.id SET NULL Parent deletion detaches children but preserves them
messages.sessionId → sessions.id CASCADE Deleting a session removes all its messages
parts.messageId → messages.id CASCADE Deleting a message removes all its parts
parts.sessionId → sessions.id CASCADE Deleting a session removes all its parts
operations.* → (no FK to spokes) Operations have no direct spoke FK — definitions are provider-independent
operation_registrations.operationId → operations.id CASCADE Definition deleted → all its registrations cascade
operation_registrations.providerId → spokes.id (polymorphic) Application-level On spoke disconnect, registrations set to status: 'inactive'. On admin spoke row deletion, registrations CASCADE. See D1/D3 in storage-spec-phase1-resolutions.md.
spokes.projectId → projects.id SET NULL Project deletion detaches spokes but preserves registration records
api_keys.ownerId → accounts.id CASCADE Deleting an account removes its API keys
audit_logs.keyId → api_keys.id SET NULL Key deletion preserves audit trail
audit_logs.ownerId → accounts.id RESTRICT Audit trails must preserve accountability; RESTRICT prevents account deletion when audit entries exist. Accounts with audit entries are deactivated via status column instead of deleted.
audit_logs.sessionId → sessions.id SET NULL Session deletion preserves audit trail
audit_logs.orgId → organizations.id SET NULL Org deletion preserves audit trail
clients.ownerId → accounts.id RESTRICT Can't delete an account that owns clients
clients.orgId → organizations.id SET NULL Org deletion detaches clients but preserves them
client_secrets.clientId → clients.id CASCADE Deleting a client removes all its secrets
mappings.sessionId → sessions.id CASCADE Deleting a session removes its mapping
mappings.spokeId → spokes.id SET NULL Spoke disconnect preserves mapping records
mappings.parentSessionId → sessions.id SET NULL Coordinator deletion detaches but preserves mapping
mappings.taskId → tasks.id SET NULL Task deletion detaches mapping but preserves it
mappings.workspaceId → workspaces.id SET NULL Workspace deletion detaches mapping but preserves it
detections.sessionId → sessions.id CASCADE Deleting a session removes its detections
detections.resolvedBy → accounts.id SET NULL Resolving account deletion preserves detection record (nullable FK + SET NULL — detection retains context without the resolver reference)
roles.parentId → roles.id SET NULL Deleting a parent role detaches children (they become standalone)
sessions.accountId → accounts.id SET NULL Deleting an account preserves sessions but detaches them (audit trail maintained)
tasks.projectId → projects.id CASCADE Deleting a project removes all its tasks
task_dependencies.dependsOnTaskId → tasks.id CASCADE Prerequisite task deletion removes its outgoing dependency edges
task_dependencies.dependentTaskId → tasks.id CASCADE Dependent task deletion removes its incoming dependency edges
call_graph_edges.sourceId → call_graph_nodes.id CASCADE Deleting a node removes its outgoing edges
call_graph_edges.targetId → call_graph_nodes.id CASCADE Deleting a target node removes its incoming edges
call_graph_nodes.operationId → operations.id SET NULL Operation definition deletion preserves call records but detaches them (nullable FK — call data retains audit value even if the operation is removed)
api_keys.rotatedToId → api_keys.id SET NULL Old key keeps its data; if new key is deleted, rotation link is broken but both keys remain

Index Reference

Table Index Type Purpose
accounts unq_accounts_email UNIQUE Email is primary identifier
accounts idx_accounts_gitea_username B-tree Gitea bridge lookup
accounts idx_accounts_display_name B-tree User search/autocomplete UIs
organizations unq_organizations_name UNIQUE Name is unique
organizations unq_organizations_slug UNIQUE Slug is unique
organizations idx_organizations_owner_id B-tree Find orgs by owner
organizations idx_organizations_gitea_org_name B-tree Gitea bridge lookup
organization_members unq_org_members_org_account UNIQUE (org_id, account_id) One membership per account per org
organization_members idx_org_members_account_id B-tree Find orgs for an account
organization_members idx_org_members_org_id B-tree Find members of an org
sessions idx_sessions_project_id B-tree Load sessions for a project
sessions idx_sessions_workspace_id B-tree Filter sessions by workspace
sessions idx_sessions_status B-tree Filter by session status
sessions idx_sessions_active Partial B-tree (WHERE status IN ('idle', 'busy', 'retry')) Efficiently find active (non-archived) sessions
sessions idx_sessions_account_id B-tree Find sessions by account
sessions idx_sessions_role_name B-tree Find sessions by role
sessions unq_sessions_slug UNIQUE Slug is unique across all sessions
sessions idx_sessions_parent_id B-tree Find child sessions of coordinator
projects idx_projects_org_id B-tree Find projects for an org
workspaces idx_workspaces_project_id B-tree Find workspaces for a project
messages idx_messages_session_id_created_at_id Composite Paginated message loading per session (opencode pattern)
parts part_session_idx B-tree Direct part queries per session
parts part_message_id_id_idx Composite (message_id, id) Load parts for a message in order
parts idx_parts_session_id_type Composite (session_id, type) Find parts by type within a session (e.g., all tool-call parts)
call_graph_nodes idx_call_graph_nodes_request_id UNIQUE Unique call correlation
call_graph_nodes idx_call_graph_nodes_operation_id B-tree Filter by operation
call_graph_nodes idx_call_graph_nodes_status B-tree Filter by status
call_graph_nodes idx_call_graph_nodes_caller_account_id B-tree Find calls by caller account
call_graph_nodes idx_call_graph_nodes_created_at B-tree Time-range queries for call graph nodes
call_graph_nodes idx_call_graph_nodes_operation_created Composite (operationId, createdAt) Operation + time queries
call_graph_nodes idx_call_graph_nodes_started_at B-tree p99 latency analysis (startedAt separate from createdAt)
call_graph_edges idx_call_graph_edges_source_id B-tree Graph traversal — find calls originating from a node
call_graph_edges idx_call_graph_edges_target_id B-tree Graph traversal — find calls targeting a node
call_graph_edges idx_call_graph_edges_source_id_type Composite (sourceId, edgeType) Find outgoing calls of a specific type
call_graph_edges unq_call_graph_edges_source_target_type UNIQUE (sourceId, targetId, edgeType) Prevent duplicate edges from retries/reconnections
operations unq_operations_namespace_name UNIQUE (namespace, name) Operation definition uniqueness by namespace+name
operations idx_operations_namespace B-tree Filter by namespace
operations idx_operations_type B-tree Filter by operation type
operation_registrations unq_operation_registrations_active UNIQUE partial (WHERE status = 'active') One active registration per provider per operation
operation_registrations idx_operation_registrations_operation_id B-tree Find registrations for an operation
operation_registrations idx_operation_registrations_provider_id B-tree Find registrations for a provider
operation_registrations idx_operation_registrations_status B-tree Filter by registration status
api_keys idx_api_keys_owner_id B-tree List keys by owner
api_keys unq_api_keys_key_hash UNIQUE Prevent duplicate key hashes (also covers idx_api_keys_key_hash — UNIQUE constraint auto-creates an index)
api_keys idx_api_keys_enabled B-tree Filter enabled/disabled keys
api_keys idx_api_keys_active Partial B-tree (WHERE revoked_at IS NULL AND enabled = true) Efficiently find active (non-revoked, enabled) keys without scanning revoked/disabled rows
audit_logs idx_audit_logs_owner_id B-tree Audit trail by owner
audit_logs idx_audit_logs_key_id B-tree Audit trail by key
audit_logs idx_audit_logs_action B-tree Filter by action type
audit_logs idx_audit_logs_created_at B-tree Paginated audit log queries
audit_logs idx_audit_logs_session_id B-tree Filter audit logs by session
audit_logs idx_audit_logs_org_id B-tree Filter audit logs by organization
clients unq_clients_name UNIQUE Client name is unique
clients idx_clients_type B-tree Find clients by type
clients idx_clients_owner_id B-tree Find clients by owner
clients idx_clients_org_id B-tree Find clients by org
client_secrets unq_client_secrets_client_key UNIQUE (client_id, key) One named secret per client
client_secrets idx_client_secrets_expires_at B-tree Find expiring secrets
mappings idx_mappings_session_id B-tree Find mapping for a session
mappings idx_mappings_parent_session_id B-tree Find children of a coordinator
mappings idx_mappings_spoke_id B-tree Find mappings for a spoke
mappings idx_mappings_task_id B-tree Find mapping for a task
mappings idx_mappings_workspace_id B-tree Workspace-scoped mapping queries
detections idx_detections_session_id B-tree Find detections for a session
detections idx_detections_anomaly_type B-tree Filter by detection type
detections idx_detections_resolved_at B-tree Find active (unresolved) detections
detections idx_detections_dedup_key B-tree Dedup lookups
spokes idx_spokes_project_id B-tree Find spokes for a project
spokes idx_spokes_status B-tree Find connected spokes
spokes idx_spokes_active Partial B-tree (WHERE status = 'connected') Efficiently find connected spokes without scanning disconnected rows
spokes idx_spokes_name B-tree Look up spoke by name
tasks unq_tasks_project_slug UNIQUE (projectId, slug) Task slugs unique within a project
tasks idx_tasks_project_id B-tree Find tasks for a project
tasks idx_tasks_project_status Composite (projectId, status) Find pending/in-progress tasks for a project
tasks idx_tasks_status B-tree Filter by task status
tasks idx_tasks_active Partial B-tree (WHERE status IN ('pending', 'in-progress', 'blocked')) Efficiently find active tasks (pending, in-progress, blocked)
tasks idx_tasks_path B-tree with text_pattern_ops Scoped queries by path prefix (e.g., LIKE 'implementation/%'). Uses text_pattern_ops operator class for locale-independent LIKE pattern matching.
tasks idx_tasks_priority B-tree Filter by priority
tasks idx_tasks_assignee B-tree Find tasks assigned to an agent
tasks idx_tasks_due_at B-tree Deadline queries
tasks idx_tasks_tags GIN Array-contains queries on tags
task_dependencies unq_task_dependencies_depends_on_task UNIQUE (dependsOnTaskId, dependentTaskId) No duplicate dependency edges
task_dependencies idx_task_dependencies_depends_on_task_id B-tree What depends on this task?
task_dependencies idx_task_dependencies_dependent_task_id B-tree What does this task depend on?
roles unq_roles_name UNIQUE Role name is unique
roles idx_roles_parent_id B-tree Find roles that inherit from a parent
roles idx_roles_mode B-tree Filter by mode (primary/subagent)

Status Enum Reference

Status enums across tables:

Table Status Values Meaning
sessions idle, busy, retry, archived Session lifecycle
sessions.roleName text Which behavioral role (e.g., "architect", "implementation-specialist"). Free-form string, not a FK constraint. See agent-roles.md and ADR-012.
spokes connected, disconnected WebSocket connection state
operations (no status column) — Definitions are persistent
operation_registrations active, inactive Provider registration lifecycle
mappings active, completed, aborted, failed Coordination workflow state
call_graph_nodes pending, running, completed, failed, aborted Call protocol lifecycle
tasks pending, in-progress, completed, failed, blocked SDD task lifecycle (matches taskgraph; transitions via hub operations)
api_keys (not an enum) enabled boolean + revokedAt timestamp + expiresAt timestamp
accounts accessLevel column admin, user, service — access level (renamed from role to avoid confusion with behavioral roles; see ADR-012)
accounts status column active, suspended, deactivated — Account lifecycle — active accounts can authenticate, suspended are admin-locked, deactivated are user-initiated shutdown
organization_members membershipLevel column owner, admin, member — org membership level (renamed from role; see ADR-012)
clients enabled boolean Enabled/disabled toggle, not a status enum

mappings.active and call_graph_nodes.pending/running are different concepts — "active" means the mapping's workflow is in progress (the coordinator is still working), while "pending"/"running" refer to the call protocol's execution state.

Cross-Table Status Mapping

Equivalent states across tables, grouped by semantic meaning:

Active/Enabled across tables: sessions.status = 'busy', spokes.status = 'connected', mappings.status = 'active', accounts.status = 'active', clients.enabled = true, api_keys.enabled = true

Inactive/Disabled across tables: sessions.status = 'archived', spokes.status = 'disconnected', mappings.status = 'aborted', accounts.status = 'suspended' OR 'deactivated', clients.enabled = false, api_keys.enabled = false

Terminal states: sessions.status = 'archived' (completed conversation), mappings.status = 'completed' (successful finish), call_graph_nodes.status = 'completed', tasks.status = 'completed'

Same-named statuses with different semantics:

  • completed in mappings = the coordination workflow finished successfully. completed in call_graph_nodes = a single call resolved. completed in tasks = an SDD task finished. These are independent — a mapping can be completed while some of its call graph nodes are failed.
  • failed in mappings = the coordination workflow errored. failed in call_graph_nodes = a call threw an error. failed in tasks = a task cannot proceed.
  • aborted in mappings = coordinator cancelled the workflow. aborted in call_graph_nodes = a call was cancelled before completion.

Valid cross-table status combinations:

  • Task in-progress ⟹ mapping active (task is being worked on, mapping is live)
  • Task completed ⟹ mapping completed (task finished, mapping records success)
  • Task failed ⟹ mapping failed (task errored, mapping records failure)
  • Task blocked ⟹ mapping active (task is waiting on dependencies, mapping stays active)
  • Session busy with no mapping ⟹ session is running outside coordination context

Note: Different domains use different status semantics. A session being busy doesn't mean the spoke is connected — they're independent states from independent lifecycles. Don't overgeneralize.

Relations

Explicit relations() definitions with one() and many() for Drizzle's relational query API:

// Key relations:
// accounts → organizations (one-to-many via ownerId)
// accounts → organization_members (one-to-many)
// organizations → organization_members (one-to-many)
// organizations → projects (one-to-many)
// organizations → clients (one-to-many, nullable FK)
// projects → workspaces (one-to-many)
// projects → sessions (one-to-many)
// workspaces → sessions (one-to-many)
// sessions → messages (one-to-many, cascade)
// messages → parts (one-to-many, cascade)
// sessions → parts (one-to-many, for direct queries)
// sessions → mappings (one-to-many)
// sessions → detections (one-to-many)
// spokes → operation_registrations (one-to-many, polymorphic FK via providerType/providerId)
// operations → operation_registrations (one-to-many, cascade)
// accounts → api_keys (one-to-many)
// api_keys → audit_logs (one-to-many)
// accounts → audit_logs (one-to-many)
// sessions → audit_logs (one-to-many)
// organizations → audit_logs (one-to-many)
// accounts → clients (one-to-many)
// clients → client_secrets (one-to-many, cascade)
// call_graph_nodes → call_graph_edges (one-to-many, both directions)
// projects → tasks (one-to-many, cascade)
// tasks → task_dependencies (one-to-many, cascade — both directions: as prerequisite and as dependent)
// tasks → mappings (one-to-many, via taskId)
// call_graph_nodes → call_graph_edges (one-to-many, both directions)