Custom Storage Adapters
Invariant decouples runtime execution semantics from physical database technology. All persistence guarantees are defined via the RuntimeStore interface in @invariant-tech/core.
The RuntimeStore Contract
To create a custom storage adapter (e.g. for MySQL, SQLite, MongoDB, or DynamoDB), implement the RuntimeStore interface:
ts
import {
validateTransitionCommit,
type RuntimeStore,
type TransitionCommit,
type ExecutionState,
type RuntimeEvent,
type Lease,
type DurableSession,
type SessionWrite,
} from "@invariant-tech/core";
export class CustomDatabaseStore implements RuntimeStore {
/**
* Atomically commits state transition, appended events, generated outbox commands,
* and command acknowledgements in a single database transaction. Enforces OCC.
*/
async commitTransition(commit: TransitionCommit): Promise<void> {
// 1. Validate the complete structural contract before BEGIN / mutation.
validateTransitionCommit(commit);
// 2. Check expectedRevision against current database row (OCC check)
// 3. Insert commit.events into event log table
// 4. Persist commit.nextState; its revision advances once per appended durable event
// 5. Insert commit.commands into transactional outbox
// 6. Update status of processed commands from commit.commandUpdates
// Must be 100% atomic (all succeed or all rollback cleanly)
}
async loadState(runId: string): Promise<ExecutionState | null> {
// Retrieve latest committed state by runId
}
async readEventLog(runId: string): Promise<RuntimeEvent[]> {
// Read ordered event history ordered by seq
}
async acquireLease(runId: string, workerId: string, ttlMs: number): Promise<Lease | null> {
// Acquire exclusive worker lock for runId
}
async renewLease(runId: string, leaseId: string, ttlMs: number): Promise<boolean> {
// Extend lease expiration
}
async releaseLease(runId: string, leaseId: string): Promise<void> {
// Release worker lock
}
async findRunnableExecutions(options: { limit: number }): Promise<string[]> {
// Query running/cancelling executions where the lease is expired or absent.
// This does not expose or claim pending commands.
}
async saveSession(write: SessionWrite): Promise<void> {
// Compare-and-swap write.session using write.expectedRevision.
// Reject an existing row owned by another userId.
}
async loadSession(sessionId: string): Promise<DurableSession | null> {
// Load durable session context
}
}Core Invariants for Custom Stores
If you write a custom adapter, it must not break these five fundamental invariants:
- Complete Preflight: Call
validateTransitionCommit()before opening the transaction. Structurally invalid proposals must cause zero database queries or mutations. - Guaranteed Atomicity: You cannot commit
statein one query andoutboxin an un-persisted async background call. If the database crashes mid-commit, zero partial state must remain. - Zero Dirty Reads:
loadState()must return the state representing the latest committed revision. - Mutual Exclusion:
acquireLease()must guarantee that only one worker can process arunIdat any instant in time. - Session Authority CAS:
saveSession()andcommitTransition().sessionmust require the expected Session revision, preserveuserIdownership, and advance the next revision exactly once. When a transition includes a Session write, execution and Session OCC must share one transaction.
Recovery contract boundary
RuntimeStore does not include portable command read/claim or arbitrary-runId runnable-work attachment methods. Implementing this interface alone does not provide end-to-end cross-process work recovery. The SDK's Session restoration path can still reconstruct a registered active .wait() boundary from stored Session and execution facts. Conform to the canonical Recovery Contract Matrix when describing adapter guarantees.
Conformance Harness
Adapter tests may use describeRuntimeStoreConformance from the dedicated @invariant-tech/core/testing subpath. It is deliberately absent from the production @invariant-tech/core root export so test-runner globals cannot leak into the normal framework surface.
ts
import { describeRuntimeStoreConformance } from "@invariant-tech/core/testing";
describeRuntimeStoreConformance("custom-store", () => ({
store: new CustomDatabaseStore(),
}));Next Steps
- Database Schema & Data Lifecycle — Compare the canonical logical model and adapter-specific encodings.
- PostgreSQL Adapter — Inspect the reference PostgreSQL implementation.
- Self-Hosting Guide — Deploy and operate Invariant with Docker Compose.
- SDK Reference — View complete TypeScript store contracts.