libSQL / Turso
Esta página aún no está disponible en tu idioma.
The libSQL backend provides all three persistence components against a
Turso database or a self-hosted
sqld, via the
@libsql/client driver:
LibSqlJournal— the event journal forPersistentActors.LibSqlSnapshotStore— snapshots to bound recovery.LibSqlDurableStateStore— key-value durable state forDurableStateActors. This is the first durable-state store in the SQLite family — the local SQLite backend ships journal + snapshot only.
Two things make it different from the other backends:
- No native binding. The driver speaks HTTP and WebSocket, so it runs on every runtime the framework supports and inside edge sandboxes that cannot load native addons at all.
- Schema-compatible with the local SQLite backend. The tables and statements are the same, so you can develop against a local file and deploy to Turso — or pull a Turso database down and open it locally — without a migration.
Like Postgres and Cassandra, it is shared across cluster nodes: any node can read or write any persistenceId.
Install
Section titled “Install”@libsql/client is an optional peer dependency — install it alongside
actor-ts:
bun add @libsql/clientThe framework lazy-imports it only when a libSQL store is first used, so it stays out of your bundle until you opt in.
Register the journal + snapshot store against the PersistenceExtension
and receive a ready-to-use durable-state store. Set the connection once
on the composite and all three components inherit it:
import { ActorSystem, ActorSystemOptions, LibSqlDurableStateStoreOptions, LibSqlJournalOptions, LibSqlSnapshotStoreOptions, PersistenceExtensionId, RegisterLibSqlPluginsOptions, registerLibSqlPlugins,} from 'actor-ts';
const systemOptions = ActorSystemOptions.create() // Select the libSQL plugins as the active journal + snapshot store. .withConfig({ 'actor-ts': { persistence: { journal: { plugin: 'actor-ts.persistence.journal.libsql' }, 'snapshot-store': { plugin: 'actor-ts.persistence.snapshot-store.libsql' }, }, }, });const system = ActorSystem.create('my-app', systemOptions);
const ext = system.extension(PersistenceExtensionId);const libSqlSnapshotStoreOptions = LibSqlSnapshotStoreOptions.create() .withKeepN(3);const registerOptions = RegisterLibSqlPluginsOptions.create() .withUrl('libsql://my-database.turso.io') .withAuthToken(process.env.TURSO_AUTH_TOKEN!) .withJournal(LibSqlJournalOptions.create() /* .withEventsTable(...).withTagsTable(...) */) .withSnapshotStore(libSqlSnapshotStoreOptions) .withDurableStateStore(LibSqlDurableStateStoreOptions.create() /* .withTable(...) */);const { durableStateStore } = registerLibSqlPlugins(ext, registerOptions);registerLibSqlPlugins registers the journal + snapshot store via the
extension (selected by the config plugin IDs above) and returns the
durable-state store. PersistenceExtension has no durable-state registry
— hand durableStateStore to your DurableStateActor settings directly
(same pattern as the Postgres and object-storage plugins).
Sharing one client
Section titled “Sharing one client”A libSQL client is a connection pool in its own right, so passing one
client is the efficient shape when all three components target the same
database:
import { createClient } from '@libsql/client/web';
const client = createClient({ url: 'libsql://my-database.turso.io', authToken: process.env.TURSO_AUTH_TOKEN,});const registerLibSqlPluginsOptions = RegisterLibSqlPluginsOptions.create() .withClient(client);registerLibSqlPlugins(ext, registerLibSqlPluginsOptions);A shared client is caller-owned: no store closes it, so close it
yourself during shutdown. Pass url / authToken instead and each store
builds its own client and closes it on close().
Self-hosted sqld
Section titled “Self-hosted sqld”const libSqlJournalOptions = LibSqlJournalOptions.create() .withUrl('http://127.0.0.1:8080');A local sqld typically needs no auth token; omit withAuthToken.
When to use it
Section titled “When to use it”- You want SQLite semantics without a native build step. No
better-sqlite3compile, no platform binaries in your image. - You run on Deno, or on an edge runtime. The only SQLite-flavoured backend that works there, since it never loads a native addon.
- You already use Turso, or want a managed SQLite with replicas near your users.
- You want durable state in the SQLite family. The only SQLite-family durable-state store.
For a single-node, local database, prefer
SqliteJournal: it talks to the file
directly, keeps prepared statements alive across calls, and pays no
network round-trip per statement.
Configuration
Section titled “Configuration”type LibSqlConnection = { url?: string; // libsql:// | http(s):// | ws(s):// authToken?: string; // Turso token; omit for a local sqld client?: LibSqlClientLike; // pre-built / shared client};
interface LibSqlJournalOptions extends LibSqlConnection { eventsTable?: string; // default 'events' tagsTable?: string; // default '<eventsTable>_tags' autoCreateTables?: boolean; // default true}interface LibSqlSnapshotStoreOptions extends LibSqlConnection { snapshotsTable?: string; // default 'snapshots' keepN?: number; // keep newest N per pid; default 3, <=0 keeps all autoCreateTables?: boolean;}interface LibSqlDurableStateStoreOptions extends LibSqlConnection { table?: string; // default 'durable_state' autoCreateTables?: boolean;}Options are validated when the store is constructed, so a bad URL or an
empty auth token fails at wiring time rather than on the first append.
Table names come from config (not user input) and are checked against a
safe-identifier pattern; everything else — persistenceIds, tags, payloads
— is passed as bind parameters (?), never string-concatenated.
Schema
Section titled “Schema”With autoCreateTables (the default), the backend runs
CREATE TABLE IF NOT EXISTS on first use. These are the same tables the
local SQLite backend creates:
CREATE TABLE events ( persistence_id TEXT NOT NULL, sequence_nr INTEGER NOT NULL, payload TEXT NOT NULL, -- JSON tags TEXT, -- CSV (also mirrored into events_tags) timestamp INTEGER NOT NULL, PRIMARY KEY (persistence_id, sequence_nr));CREATE TABLE events_tags ( -- indexed tag lookups for projections persistence_id TEXT NOT NULL, sequence_nr INTEGER NOT NULL, tag TEXT NOT NULL, timestamp INTEGER NOT NULL, PRIMARY KEY (tag, timestamp, persistence_id, sequence_nr));CREATE TABLE events_meta ( -- compaction high-water mark persistence_id TEXT PRIMARY KEY, deleted_to INTEGER NOT NULL);CREATE TABLE snapshots ( persistence_id TEXT NOT NULL, sequence_nr INTEGER NOT NULL, payload TEXT NOT NULL, timestamp INTEGER NOT NULL, PRIMARY KEY (persistence_id, sequence_nr));CREATE TABLE durable_state ( persistence_id TEXT PRIMARY KEY, revision INTEGER NOT NULL, payload TEXT NOT NULL, timestamp INTEGER NOT NULL);Pre-provision these and set autoCreateTables: false if the database role
cannot run DDL.
Concurrency model
Section titled “Concurrency model”- Journal append reads the current head and inserts inside one
interactive transaction; the caller’s
expectedSeqis checked against that head. A racing writer that slips through trips the primary key, and the resultingSQLITE_CONSTRAINTerror is translated toJournalConcurrencyErroras a backstop — so the guarantee holds even when the transport cannot give strict isolation. - Durable-state CAS uses the
revisioncolumn: a create (expectedRevision === 0) isINSERT … ON CONFLICT DO NOTHING, and an update isUPDATE … WHERE revision = expected. Zero rows affected ⇒DurableStateConcurrencyError, with the current revision read back for the caller.
Pitfalls
Section titled “Pitfalls”Where to next
Section titled “Where to next”- SQLite journal — the local sibling, same schema, no network.
- PostgreSQL — relational, with transactional isolation and no per-statement round-trip.
- MariaDB — the MySQL-family sibling.
- Durable state — the state-oriented alternative to event sourcing.
- Snapshots — bound the recovery scan.
