Storage Adapters
Overview
As of v0.3, createField() accepts an adapter option that determines where entries and the event log actually live. Three adapters ship with @akashikprotocol/core:
| Adapter | Durable | Use for |
|---|---|---|
MemoryAdapter | No | Tests, single-run scripts, the default when no adapter is given |
FileAdapter | Yes | Local development, single-machine services |
PostgresAdapter | Yes | Production, multi-machine / multi-process deployments |
All three implement the same interface, so switching adapters never changes application code — only how createField() is constructed.
MemoryAdapter
The default. State lives in a JS array and disappears when the process exits.
import { createField } from '@akashikprotocol/core'
const field = createField()
// Uses MemoryAdapter implicitly — no configuration needed
FileAdapter
Persists entries and the event log to a JSONL file.
import { createField } from '@akashikprotocol/core'
import { createFileAdapter } from '@akashikprotocol/core/file'
const adapter = createFileAdapter({ path: './field.jsonl' })
await adapter.init()
const field = createField({ adapter })
Caveat: the file path is the Field's identity — two createField() calls pointed at different paths are two different Fields, and pointed at the same path, they share state. Relative paths resolve against the current working directory at construction time, not the location of the source file. Use absolute paths in production code.
PostgresAdapter
Backed by a Postgres table, for concurrent access from multiple machines or processes.
import { createField } from '@akashikprotocol/core'
import { createPostgresAdapter } from '@akashikprotocol/core/postgres'
const adapter = createPostgresAdapter({
connectionString: process.env.DATABASE_URL,
fieldId: 'production',
})
await adapter.init()
const field = createField({ adapter })
| Option | Required | Description |
|---|---|---|
connectionString | A Postgres connection string. Provide this or pool. | |
pool | An existing pg Pool instance, if your app already manages one. | |
fieldId | Namespaces multiple Fields within one database. Defaults to "default". |
The pg package is an optional peer dependency — only required if you use this adapter.
Writing a custom adapter
All adapters implement the same StorageAdapter interface, so a conforming custom backend (Redis, SQLite, a hosted API) is possible. Two invariants every adapter must uphold:
- Atomic batch writes —
supersede()produces two events (a status change on the predecessor and a record for the new entry). Both must succeed or both must fail; a partial write corrupts the chain. - Serialized sequence assignment — concurrent writers must not hand out overlapping
seq/epochvalues. Gaps or collisions cause incremental readers (replay({ sinceSeq }),attune({ since_epoch })) to silently skip events.
Snapshots, if an adapter implements them, are optional optimizations — a corrupt snapshot should return null and force a rebuild from the event log rather than fail the read.
Next
- createField() reference — the
adapteroption - replay() reference — reading back what an adapter persisted
- The Field — what persistence does and doesn't change about Field semantics