- Update architecture docs to reflect pivot from @libsql/client to Honker - Fold @alkdev/drizzlebox Phase 0 into src/sqlite/utils/ (ADR-046) - Add HonkerEventTarget adapter for pubsub TypedEventTarget (ADR-047) - Replace hand-written CRUD with OperationSpec generation (ADR-048) - Resolved OQ-26: Honker replaces Redis for single-node pub/sub (POC validated) - Updated OQ-17, OQ-18, OQ-19 for OperationSpec repository surface - Added OQ-30 (composite event target), OQ-31 (consumer naming), OQ-32 (Drizzle Kit) - POC results: adapter buildable, same-process pub/sub works, transactional outbox semantics confirmed, concurrent listeners/streams work - Research doc at docs/research/pivot-honker-sqlite-adapter.md
3.1 KiB
status, date, supersedes
| status | date | supersedes |
|---|---|---|
| accepted | 2026-06-01 | ADR-033 (partially — OQ-17, OQ-18, OQ-19 updated) |
ADR-048: OperationSpecs as Repository Surface
Context
ADR-033 established JSON path queries with hand-written CRUD for v1. The question was whether the repository layer would be hand-written query functions, auto-generated from table definitions, or something else.
The @alkdev/operations package provides OperationSpec — a serializable
descriptor for a query, mutation, or subscription operation. Hubs and spokes
register specs in an OperationRegistry along with handlers. The operations
runtime handles execution, call protocol routing, subscriptions, access
control, and validation.
This presents a natural alternative to hand-written CRUD: storage outputs
OperationSpec[] describing CRUD operations per table, and the consumer
wires them into the operations registry.
Decision
Storage does not ship a "repository layer" of hand-written query functions.
Instead, it outputs OperationSpec[] per table — flat arrays describing
CRUD operations. The consumer (hub/spoke) imports these specs, registers
handlers, and the operations runtime handles execution.
// Storage defines table + operation contracts
export const graphTypes = sqliteTable("graph_types", { ... });
export const graphTypeSpecs: OperationSpec[] = [
{ name: "create", namespace: "graph_types", type: "mutation", ... },
{ name: "find", namespace: "graph_types", type: "query", ... },
{ name: "list", namespace: "graph_types", type: "query", ... },
{ name: "update", namespace: "graph_types", type: "mutation", ... },
{ name: "delete", namespace: "graph_types", type: "mutation", ... },
];
// Hub registers specs + handlers
for (const spec of graphTypeSpecs) {
registry.registerSpec(spec);
registry.registerHandler(`${spec.namespace}.${spec.name}`, handler);
}
The handler is consumer-provided. Storage defines the contract (input/output schemas, operation type); the hub provides the execution (Drizzle queries, Honker transactions, notifications).
@alkdev/operations is a type-only peer dependency of storage. No circular
dependency.
Consequences
- No hand-written repository functions — storage avoids the maintenance burden of typed CRUD code. The contract is the OperationSpec.
- Consumer owns execution — the hub decides how to handle each operation (raw Drizzle query, with Honker notification, with access control, etc.). Storage doesn't execute queries.
- Subscriptions for free — OperationSpec supports
type: "subscription". Table-level change streams become operation subscriptions rather than custom event handling. - Clean dependency graph — storage depends on operations only for the
OperationSpectype (peer dep). No runtime dependency. - Attribute queries remain JSON path — for metagraph tables where
attributes are dynamic JSON,
json_extract()is still the query mechanism. Domain-specific tables with native columns produce simpler specs. - Supersedes ADR-033 partially — the "hand-written CRUD" part is replaced. The "JSON path queries for attributes" part still applies to metagraph tables.