# ADR 0128: SQLite runtime storage is a leaf adapter

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0128-sqlite-runtime-store-is-a-leaf-adapter.md

---

# ADR 0128 — SQLite runtime storage is a leaf adapter

## Context

The public `AgentRuntimeStoreDriver` makes database ownership explicit, but an
embedded durable runtime otherwise has to repeat the same head, run, admission,
history and recovery mapping. SQLite is available in both supported runtimes
through different built-in modules: `bun:sqlite` and `node:sqlite`. Importing
either from the neutral runtime would make the other runtime fail at module
resolution.

A synchronous connection also has a lock hazard. If one async transaction holds
a write lock while its callback yields, a second synchronous writer on the same
JavaScript thread can block that thread waiting for the lock. The first callback
then cannot resume to release it. A positive busy timeout only bounds the hang;
it does not make progress safe.

## Decision

Ship one normalized SQLite implementation behind two leaf entrypoints:
`stitchkit/agent-runtime/sqlite/bun` and
`stitchkit/agent-runtime/sqlite/node`. The shared implementation accepts a
minimal synchronous SQLite boundary; the leaves adapt Bun's `Database` and
Node's `DatabaseSync`. Neither built-in module enters
`stitchkit/agent-runtime`, its browser entrypoint or the other runtime's leaf.

The adapter owns only tables prefixed `stitchkit_agent_runtime_` and records its
schema version in its own metadata table. It does not use SQLite `user_version`,
inspect application tables or repair an unversioned partial Stitchkit schema.
Initialization is additive for a fresh database and refuses an unknown version.

One adapter instance serializes transactions. Each leaf configures
`busy_timeout = 0`, so a competing connection fails immediately with SQLite's
lock error instead of blocking the JavaScript thread. The application may retry
that explicit failure with its own bounded policy. `close()` first refuses new
operations, drains the instance queue and then closes the owned connection.

## Consequences

- Bun uses its built-in SQLite binding; Node requires a runtime that provides
  `node:sqlite` (Node 22.5 or newer). No optional package is installed.
- Durable state, idempotency, queue priority, terminal evidence, compaction and
  recovery use the existing reducer and public conformance contract.
- Two writers may contend, but contention fails promptly and never reports a
  false commit. Cross-process retry and scheduling remain application policy.
- The adapter proves process-state durability, not exactly-once external
  effects. Product rows and transactional outboxes remain application-owned.
