Storage
Per-instance SQLite persistence — pg_sqlite BYTEA in Postgres and S3-backed actor state.
Actor instances store private durable state in per-instance SQLite databases. The Rust kernel abstracts two storage backends behind a StorageBackend trait — selected by YOKE_STORAGE at runtime start.
YOKE_STORAGE
| Value | Behavior |
|---|---|
pg_sqlite | Serialized SQLite files stored as BYTEA in Postgres actor_sqlite table |
s3 | SQLite files in S3; local cache directory for rusqlite execution only |
YOKE_STORAGE is required. There is no local-file-only production mode.
pg_sqlite is not the PostgreSQL pg_sqlite extension. Yoke serializes entire SQLite database files into a BYTEA column (actor_sqlite.db). SQLite executes locally in the worker process; persistence is the serialized blob in Postgres.
Postgres actor_sqlite table
actor_sqlite (
tenant_id,
instance_id,
store_id, -- default instanceId; supports sqliteStoreId segments
db BYTEA, -- serialized SQLite file
version, -- optimistic lock for state_cas
updated_at
)On instance start: load BYTEA → open rusqlite in memory/temp → serve sqlite_exec and state_* ops.
On write: execute SQL → serialize → upsert BYTEA.
S3 backend
Objects stored at:
{YOKE_S3_PREFIX}/instances/{instance_id}/state.db| Env | Purpose |
|---|---|
YOKE_S3_BUCKET | Required for S3 mode |
YOKE_S3_PREFIX | Key prefix (optional) |
YOKE_S3_SYNC | lazy (default) or strict |
Sync modes
YOKE_S3_SYNC | Behavior |
|---|---|
lazy | Upload after writes; flush_all() on shutdown |
strict | fsync local state.db before each S3 put_object |
Guarantee: With strict, successful state_set / sqlite_exec / state_cas implies local file durability before upload begins. S3 durability follows AWS semantics. Crashes between local write and upload may lose data in lazy mode.
SDK surface (identical in both modes)
| API | Backing |
|---|---|
ctx.state.get/set/update | __yoke_state table via state_get, state_set, state_cas ops |
ctx.sqlite.exec | Arbitrary SQL via sqlite_exec op |
Never write __yoke_state directly — use ctx.state.
state_cas
Atomic compare-and-swap for optimistic locking:
state_get → { data, version }
state_cas(store_id, expected_version, new_data) → { data, version } | conflictThe worker RPC exposes state_cas (not state_update). ctx.state.update() wraps this.
sqliteStoreId
Actors can partition SQLite into logical stores per invocation:
defineActor({
sqliteStoreId: (ctx) => `${ctx.instanceId}:${ctx.config.shardKey}`,
// ...
});Each store_id gets its own row in actor_sqlite or S3 object.
Blob storage (conversations)
Conversation media blobs use S3 when YOKE_STORAGE=s3:
POST /v1/tenants/:tenant_id/blobsRequires YOKE_S3_BUCKET. Metadata in Postgres blobs table; bytes in S3.
Choosing a backend
Best for: single-tenant dev, smaller deployments, strongest single-system durability. One Postgres connection handles stream, queues, and actor state.
Implementation layers
| Layer | Role |
|---|---|
| Storage dispatch | Routes ops to the active backend (pg_sqlite or s3) |
| SQLite engine | rusqlite open, state_cas, exec |
| Postgres backend | BYTEA read/write for pg_sqlite mode |
| S3 backend | S3 get/put, local cache |