Data Persistence API
Purpose and Scope
The Data Persistence API is the contract that lets a Flue runtime use a custom database without changing agent, workflow, or channel code. Adapter authors implement the exported TypeScript interfaces from @flue/runtime/adapter, while application authors usually select an adapter through a source-root db.ts. The page is written for developers building or reviewing an adapter: it explains what the runtime expects to call, which durable stores must be returned, and which correctness properties must hold when multiple submissions, streams, or recoveries interact concurrently.
Sources: apps/docs/src/content/docs/api/data-persistence-api.md
Persistence in Flue is not a generic key-value cache. It backs canonical agent conversation streams, accepted direct prompts and dispatch(...) submissions, attachments, workflow run records, and event streams. Because those records define observable runtime behavior, every backend must implement the same contract even when the underlying database primitives differ. A SQL adapter may use transactions, a document database may use conditional writes, and a cloud store may use compare-and-set operations, but the externally visible result must preserve atomic admission, ownership, ordering, and recovery semantics.
Sources: apps/docs/src/content/docs/api/data-persistence-api.md
Relevant Source Files
apps/docs/src/content/docs/api/data-persistence-api.md— Reader-facing reference for adapter authors. It defines the exported adapter and store interfaces, startup lifecycle expectations, schema-versioning policy, and durable behavior expected from submission and stream storage.
Adapter Entry Point
The top-level contract is PersistenceAdapter. It has one required method, connect(), which returns a PersistenceStores object or a promise for one. It may also provide migrate() and close(). Flue calls migrate() once at startup when present, then awaits connect() once. This means a broken database configuration is expected to fail during boot instead of surfacing later as partial runtime failures. When shutdown occurs, Flue calls close() if the adapter exposes it, giving connection pools, clients, or file handles a clear cleanup point.
Sources: apps/docs/src/content/docs/api/data-persistence-api.md
interface PersistenceAdapter {
connect(): PersistenceStores | Promise<PersistenceStores>;
migrate?(): void | Promise<void>;
close?(): void | Promise<void>;
}The returned PersistenceStores object is the runtime’s durable boundary. It must include executionStore, runStore, eventStreamStore, conversationStreamStore, and attachmentStore. These store names are intentionally domain-specific: they separate submission lifecycle state from conversation transcript state, workflow run state, runtime event history, and external payload storage. That separation matters for adapter design because a session row is not the source of truth for messages; the conversation stream is. Similarly, accepted work and workflow observations are separate concerns even if a backend stores them in related tables or collections.
Sources: apps/docs/src/content/docs/api/data-persistence-api.md
interface PersistenceStores {
readonly executionStore: AgentExecutionStore;
readonly runStore: RunStore;
readonly eventStreamStore: EventStreamStore;
readonly conversationStreamStore: ConversationStreamStore;
readonly attachmentStore: AttachmentStore;
}Store Responsibilities
AgentExecutionStore contains submission lifecycle state and exposes its submission-specific responsibilities through submissions: AgentSubmissionStore. The documentation is explicit that conversation transcripts do not live in execution session rows. That distinction prevents an adapter from treating an agent instance as one mutable blob. Accepted prompts, dispatch inputs, claims, leases, and terminal states belong to the submission lifecycle, while the canonical messages and stream offsets belong to ConversationStreamStore. A correct adapter therefore models ordered work admission and append-only conversation records as related but independent durable facts.
Sources: apps/docs/src/content/docs/api/data-persistence-api.md
AgentSubmissionStore is the most concurrency-sensitive part of the visible contract. It owns ordered admission, claim ownership, turn journals, settlement obligations, recovery, attempt markers, and lease renewal for direct prompts and dispatch(...) input. Admission is idempotent: admitDispatch() is idempotent by dispatch id, and an exact replay returns the existing admission. If the same id is reused with a different payload, the store must report a conflict. admitDirect() provides the same idempotent admission behavior for direct prompts.
Sources: apps/docs/src/content/docs/api/data-persistence-api.md
Claiming work is also fenced by ordering rules. claimSubmission() atomically changes a queued submission to running only when it is the first unsettled submission for that session, and listRunnableSubmissions() returns at most one queued head per session in admission order. This is the contract that lets Flue maintain per-session sequencing while still scanning for runnable work across many sessions. The documentation also states that sessions are append-only for the lifetime of the agent instance and that there is no per-session deletion operation in the contract.
Sources: apps/docs/src/content/docs/api/data-persistence-api.md
Concurrency, Recovery, and Schema Versioning
The adapter contract is written around durable execution rather than best-effort persistence. Lifecycle transitions are gated by the owning attempt, so input application, recovery requests, requeue-before-input, completion, and failure must reject stale attempts. The first terminal state wins. Recovery replaces a running attempt through a single fenced compare-and-set that preserves attempt ownership. Settlement reservation records the exact canonical settlement before finalization, while attempt markers and leases provide durable evidence for recovery and ownership.
Sources: apps/docs/src/content/docs/api/data-persistence-api.md
Schema handling is part of correctness, not an optional migration convenience. An adapter must record its schema or format version when it creates storage and reject every mismatch before reading or writing data. The documented pre-1.0 format is schema v7 and is reset-only: stores created by another version should be cleared rather than migrated in place. Adapter authors should use FLUE_SCHEMA_VERSION, assertSupportedFlueSchemaVersion(), and PersistedSchemaVersionError from @flue/runtime/adapter so the database’s recorded format is checked consistently with the runtime package.
Sources: apps/docs/src/content/docs/api/data-persistence-api.md
Compact Reference
| Component | Required behavior | Notes |
|---|---|---|
PersistenceAdapter.connect() | Return or resolve PersistenceStores. | Called once after optional migration. |
PersistenceAdapter.migrate?() | Prepare or verify storage. | Called once at startup when present. |
PersistenceAdapter.close?() | Release resources. | Called during shutdown when present. |
PersistenceStores.executionStore | Provide agent execution state. | Contains submission lifecycle state through submissions. |
PersistenceStores.runStore | Provide workflow run storage. | Part of the required store bundle. |
PersistenceStores.eventStreamStore | Provide runtime event stream storage. | Part of the required store bundle. |
PersistenceStores.conversationStreamStore | Provide canonical conversation stream storage. | Conversation transcripts live here, not in session rows. |
PersistenceStores.attachmentStore | Provide attachment payload storage. | Part of the required store bundle. |
AgentSubmissionStore.admitDispatch() | Idempotently admit a dispatch by dispatch id. | Exact replay returns existing admission; mismatched payload conflicts. |
AgentSubmissionStore.admitDirect() | Idempotently admit a direct prompt. | Mirrors dispatch admission behavior. |
AgentSubmissionStore.claimSubmission() | Atomically claim the first unsettled queued submission for a session. | Must reject claims that violate session ordering. |
AgentSubmissionStore.listRunnableSubmissions() | Return queued runnable heads. | At most one queued head per session, ordered by admission. |
Implementation Guidance
When implementing an adapter, start from the observable contract rather than from a preferred table layout. Decide which native primitive provides atomic admission, which primitive fences attempt ownership, and which transaction or conditional-write boundary preserves settlement and recovery invariants. If the backend cannot express those operations directly, the adapter must add serialization or another coordination strategy so callers still see the same behavior. This is especially important for direct prompts and dispatches, because duplicate delivery and recovery are expected runtime realities rather than exceptional cases.
Sources: apps/docs/src/content/docs/api/data-persistence-api.md
Application authors normally do not implement these interfaces directly. On Node.js, they provide a source-root db.ts that exports a PersistenceAdapter, and Flue wires that adapter into the generated server entry at build time. On Cloudflare, the official documentation describes Durable Object SQLite as the generated persistence mechanism instead of db.ts. If a custom adapter is used, typecheck it against the package exports from @flue/runtime/adapter; the documentation states that when the page and package differ, the package wins.
Sources: apps/docs/src/content/docs/api/data-persistence-api.md
Next Steps
If you are choosing persistence for an application, read the Database guide before writing a custom adapter. If you are authoring an adapter, implement the PersistenceAdapter lifecycle first, add schema-version stamping, then test admission idempotency, claim ordering, stale-attempt rejection, lease renewal, and recovery fencing under concurrency. After the core execution and stream stores behave correctly, wire the adapter through db.ts in a Node target and verify that startup fails cleanly for unreachable databases or schema mismatches.
Sources: apps/docs/src/content/docs/api/data-persistence-api.md