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.
7.2 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-04-19 |
Table Schemas: Spokes & Operations
Spoke registration and operation specification tables. For cross-cutting reference (cascade behavior, index reference, status enums, relations), see table-reference.md. For design decisions, see ../../../decisions/. For spoke architecture, see ../../spoke-runner.md.
spokes
Spoke registrations. When a spoke connects to the hub via WebSocket, it calls hub.register with its details and operation list. The hub creates a spoke record and registers the operations. When the spoke disconnects, the record is updated with status: "disconnected".
| Column | Type | Notes |
|---|---|---|
| commonCols | — | id, metadata, createdAt, updatedAt |
| name | text NOT NULL | Spoke display name |
| status | text NOT NULL | Enum: connected, disconnected. Default: connected |
| spokeType | text NOT NULL | Spoke type: dev-env, client, compute |
| projectId | text | FK → projects.id (nullable — some spokes aren't project-scoped) |
| lastHeartbeat | timestamp with tz | Last heartbeat timestamp |
| hostInfo | jsonb | Host metadata ({ os, arch, nodeVersion, memory, cpu }) |
| connectedAt | timestamp with tz | When the spoke connected |
| disconnectedAt | timestamp with tz | When the spoke disconnected (null if still connected) |
Indexes: idx_spokes_project_id on (projectId), idx_spokes_status on (status), idx_spokes_name on (name) — look up spoke by name, idx_spokes_active partial on (id) WHERE status = 'connected' — efficiently find connected spokes.
No reconnecting status: Spoke reconnection is handled at the WebSocket layer, not in the database. When a spoke disconnects, its status becomes disconnected. When it reconnects, it's a new connection — the spoke row is updated back to connected with a new connectedAt. Transient reconnection attempts don't need a database state; they're a transport concern.
If monitoring of reconnection attempts is needed, use the call graph (a hub.register call from the spoke) or observability events (WebSocket reconnection logs), not a database status.
No capabilities column on spokes: A spoke's capabilities are its registered operations. Query operation_registrations filtered by providerId and status = 'active' to find what a connected spoke can do. The operations table holds the definitions. See ADR-006 in decisions/.
Relationship to operations and registrations: When a spoke calls hub.register with an operations list, the hub creates or finds operations rows (definitions) for each operation, then creates operation_registrations rows linking the spoke to those definitions. When the spoke disconnects, registrations are set to inactive but definitions persist. See the operations and operation_registrations tables below.
Input mapping from hub.register: The hub.register operation (see spoke-runner.md) accepts { spokeId, operations[], spokeType, project, hardware }. This maps to the spokes table columns as: spokeId → id, spokeType → spokeType, project → projectId (looked up by project identifier), hardware → hostInfo. The name field may be derived from the spoke's configuration or provided separately. Each operation in operations[] maps to an operations row (definition, created or found by namespace+name) and an operation_registrations row (provider binding, linking the spoke to the definition).
operations
Operation definitions — what an operation IS. These persist independently of spoke connections. Multiple providers can register the same operation (by namespace+name); they share the definition.
| Column | Type | Notes |
|---|---|---|
| commonCols | — | id, metadata, createdAt, updatedAt |
| namespace | text NOT NULL | Post-remap identifier (e.g., dev.{spokeId}.fs.read) |
| name | text NOT NULL | Operation name within namespace (e.g., fs.read, call) |
| type | text NOT NULL | QUERY, MUTATION, SUBSCRIPTION |
| description | text | Human-readable description |
| inputSchema | jsonb NOT NULL | TypeBox schema for input |
| outputSchema | jsonb | TypeBox schema for output |
| errorSchemas | jsonb | Array of error type schemas |
| accessControl | jsonb | Access control definition |
| tags | jsonb | String array for search/filter |
Unique constraint: CREATE UNIQUE INDEX unq_operations_namespace_name ON operations (namespace, name) — operation definitions are unique by namespace+name, regardless of how many providers register them.
Indexes: idx_operations_namespace on (namespace), idx_operations_type on (type).
operation_registrations
Provider registrations — which spoke/client PROVIDES an operation right now. Ephemeral data: these reflect the current runtime state of who can handle a call.
| Column | Type | Notes |
|---|---|---|
| commonCols | — | id, metadata, createdAt, updatedAt |
| operationId | text NOT NULL | FK → operations.id (CASCADE — deleting a definition removes all its registrations) |
| providerType | text NOT NULL | spoke or client — which provider type |
| providerId | text NOT NULL | FK → spokes.id when providerType is spoke; FK → clients.id when providerType is client |
| preRemapNamespace | text | The original namespace before remapping (e.g., dev for dev.{spokeId}.fs.read). Stored for traceability. |
| preRemapName | text | The original name before remapping |
| status | text NOT NULL | active or inactive. Default: active. Set to inactive on disconnect, re-activated on reconnect. |
| metadata | jsonb | Provider-specific metadata (version, health, latency hints) |
Unique constraint: CREATE UNIQUE INDEX unq_operation_registrations_active ON operation_registrations (operationId, providerType, providerId) WHERE status = 'active' — only one active registration per provider per operation.
Indexes: idx_operation_registrations_operation_id on (operationId), idx_operation_registrations_provider_id on (providerId), idx_operation_registrations_status on (status).
Spoke registration lifecycle: When a spoke connects and registers:
- Creates/updates the
spokesrow - For each operation the spoke provides:
- Creates or finds the
operationsrow (by namespace+name). If this is a new spoke instance providing a known operation, the definition already exists. - Creates an
operation_registrationsrow linking the spoke to the operation definition, withstatus: 'active'and the pre-remap identifiers.
- Creates or finds the
When a spoke disconnects:
- Updates the
spokesrow tostatus: "disconnected" - Sets all the spoke's
operation_registrationsrows tostatus: "inactive" - Aborts in-flight calls via call protocol cascading
- Operation definitions (in
operations) are never deleted on disconnect — they persist for audit and potential reconnection.
When an admin deletes a spoke row (rare):
operation_registrationswith thatproviderIdare CASCADE deleted (ephemeral data, follows D1 cascade policy for ephemeral config)- If no other registrations exist for an operation, its definition may be cleaned up separately