Files
hub/docs/architecture/storage/spokes.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

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: spokeIdid, spokeTypespokeType, projectprojectId (looked up by project identifier), hardwarehostInfo. 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:

  1. Creates/updates the spokes row
  2. For each operation the spoke provides:
    • Creates or finds the operations row (by namespace+name). If this is a new spoke instance providing a known operation, the definition already exists.
    • Creates an operation_registrations row linking the spoke to the operation definition, with status: 'active' and the pre-remap identifiers.

When a spoke disconnects:

  1. Updates the spokes row to status: "disconnected"
  2. Sets all the spoke's operation_registrations rows to status: "inactive"
  3. Aborts in-flight calls via call protocol cascading
  4. Operation definitions (in operations) are never deleted on disconnect — they persist for audit and potential reconnection.

When an admin deletes a spoke row (rare):

  1. operation_registrations with that providerId are CASCADE deleted (ephemeral data, follows D1 cascade policy for ephemeral config)
  2. If no other registrations exist for an operation, its definition may be cleaned up separately