Turso (beta)
The Rust rewrite of SQLite (@tursodatabase/database) with MVCC concurrent writes via BEGIN CONCURRENT — OR remote Turso Cloud (libsql://) with embedded-replica sync via @libsql/client. Routed by URL scheme. SQLite-compatible SQL. Single-node local, beta. No generated columns, no FTS; numeric ids drop AUTOINCREMENT; no Alpine/musl or Intel-mac prebuilts on the local engine.
Turso is the Rust rewrite of SQLite (@tursodatabase/database, formerly Limbo). It speaks SQLite's SQL dialect, file format, and a better-sqlite3-shaped driver — so the framework reuses the entire SQLite-family store and SQL compiler. The ONE thing it adds over the sqlite dialect is the reason to pick it: MVCC concurrent writes via BEGIN CONCURRENT. Where sqlite serializes every write through a single connection, turso runs a connection POOL where multiple transactions commit concurrently; a write-write conflict is detected and the framework retries it transparently.
The turso dialect has two live backends, routed by the connection URL scheme — not two versions of the same thing, but a real config choice:
- Local Rust engine (
file:/:memory:) — the embedded@tursodatabase/databaseengine described above: pooled, single-process, MVCC concurrent writes. beta (0.x). - Remote Turso Cloud (
libsql:///https:///wss://) — a connection to a hosted Turso Cloud database over the official@libsql/client, with an auth token. Also supports an embedded replica: a localfile:copy that periodically syncs from a remote primary (local-latency reads, writes forwarded to the primary).
Treat the local engine as "SQLite for a single node that needs real write concurrency", not a production-hardened engine; treat the remote/replica backend as "your app runs against managed Turso Cloud". For multi-instance scale-out with sub-second local reactivity use postgres / mysql / mariadb / mssql; for a rock-stable single-process embed use sqlite.
Configuration
The URL scheme picks the backend — local engine vs remote Turso Cloud vs embedded replica:
DB_DIALECT=turso
# ── Local Rust engine ─────────────────────────────────────────
DB_URL=file:./db.turso # relative path (resolved against cwd)
DB_URL=file:/abs/path/db.turso
DB_URL=:memory: # ephemeral, pool forced to size 1 (see below)
DB_MAX_CONNECTIONS=8 # pool size = the MVCC write-concurrency knob. Default 4.
# ── Remote Turso Cloud ────────────────────────────────────────
DB_URL=libsql://your-db.turso.io # or https:// / wss://
DB_AUTH_TOKEN=<token> # from `turso db tokens create <db>` — never logged/baked
# ── Embedded replica (local file that syncs from a remote primary) ─
DB_URL=file:./replica.db # the LOCAL replica file
DB_SYNC_URL=libsql://your-db.turso.io # the remote primary it syncs FROM
DB_AUTH_TOKEN=<token>
DB_SYNC_INTERVAL=30 # optional: pull-sync every N seconds (else sync on boot only)The scheme routes the connection: file: / :memory: → the local Rust engine; libsql:// / https:// / wss:// → remote Turso Cloud (needs DB_AUTH_TOKEN); a file: URL paired with DB_SYNC_URL → an embedded replica. So the same file: scheme means "local engine" without a sync URL and "libsql embedded replica" with one. A remote URL without an auth token, or a DB_SYNC_URL without one, fails loud at boot with an actionable message. The token comes from DB_AUTH_TOKEN (alias TURSO_AUTH_TOKEN) — it is never logged or baked into the build.
Remote Turso Cloud + embedded replicas
The remote backend connects through the official @libsql/client:
- Remote Turso Cloud (
libsql:///https:///wss://): every statement runs against your hosted Turso database;ctx.store.transactional(...)opens an interactive libsqltransaction('write'). Turso Cloud serialises writes server-side (there's no client-sideBEGIN CONCURRENThere — that's the local engine's mechanism), and a transient busy/conflict is retried by the same transaction-retry the local engine uses. - Embedded replica (
file:+DB_SYNC_URL): the client keeps a local copy of the database that reads with local latency and syncs from the remote primary. The framework does an initialclient.sync()at boot so the first reads see a warm replica; ifDB_SYNC_INTERVALis set, the driver keeps pulling on that cadence. Writes are forwarded to the primary. OptionalDB_READ_YOUR_WRITES(default on) makes a local read after a write wait until the replica has caught up;DB_ENCRYPTION_KEYencrypts the local replica file at rest.
Everything above this seam is identical to the local engine — the same reused SQLite-family DataStore, the same schema/query/mutation code. Only the transport differs.
The local Rust engine and the remote/replica backend are a deliberate config choice, not a versioned split. Pick
file:/:memory:for an embedded single-process DB with MVCC write concurrency; picklibsql://(± an embedded replica) to run against managed Turso Cloud.
MVCC is mandatory — journal_mode=experimental_mvcc
BEGIN CONCURRENT is only accepted when MVCC is enabled, so every pooled connection runs PRAGMA journal_mode=experimental_mvcc at open. This is not optional for the dialect — it's what the whole thing is for. The store's DML transactions run as BEGIN CONCURRENT, so every ctx.store.transactional(...) (i.e. every mutation) opens a concurrent transaction. DDL keeps plain BEGIN (Turso rejects DDL inside a concurrent transaction), so the @effect/sql client defaults to BEGIN and the store upgrades its own transactions per-fiber — migrations (including the workflow engine's) still work.
Concurrent writes + automatic retry
Two mutations that touch the same row run on two different pooled connections, each with its own MVCC snapshot. The loser of the commit race gets a Write-write conflict; the framework's transaction retry (exponential backoff, a few attempts) replays the whole transaction body on a fresh snapshot that now sees the winner's commit. Your handler code is unchanged — write a normal transactional mutation and the concurrency + retry happen underneath.
Size the pool (DB_MAX_CONNECTIONS) to the concurrent-mutation count you expect. :memory: is per-connection (each connection is a private database), so the pool is forced to size 1 there — :memory: is for tests, not for exercising concurrency.
Driver: @tursodatabase/database (native NAPI)
Async driver (connect() → prepare() → run/get/all), wrapped in Effect. The framework's decodeRowsFromSchema post-processor handles the same SQLite read quirks as the sqlite dialect:
boolean()columns: 0/1 → false/truejson()columns: TEXT → object (JSON.parse)timestamp()/date()columns: ISO string → Date
On write, the same coercion converts Date → ISO string and boolean → 0/1 before binding.
Prebuilt binaries ship for linux-x64-gnu, linux-arm64-gnu, win32-x64-msvc, darwin-arm64 only — there is NO Alpine/musl or Intel-mac binary. A Node Docker image on Alpine will fail to install the driver; use a glibc base image (node:22-bookworm-slim, etc.). This applies to the local engine only — the remote / embedded-replica backend goes through @libsql/client (pure JS + its own bindings) and is not bound by the local engine's prebuilt matrix.
No cluster — runnerStorage: 'memory'
Like sqlite, Turso is single-node here. The workflow engine boots with runnerStorage: 'memory': workflows run durably within ONE process (crash + restart resumes from the journal), multi-replica scale-out is not possible, and DB_REPLICA_URLS is a no-op with a warning. (The pool gives concurrent writes on one node — it does NOT give horizontal scale-out.)
CDC — in-process EventEmitter
Same as sqlite: an in-process Node EventEmitter fans insert/update/delete events to the dispatcher. Sub-millisecond, single-process. No LISTEN/NOTIFY, no triggers.
Not supported on Turso (beta gaps)
These are SQLite features the sqlite dialect has but Turso's MVCC mode does not. The framework fails LOUD at migrate rather than emit DDL the engine rejects:
- Generated columns (
.generatedAs(...)). Turso MVCC rejects STORED/VIRTUAL generated columns — migrate throws a clear error. Drop the column or usesqlite/postgres. - Full-text search (
.fullTextIndex(...)). MVCC has no virtual tables (FTS5) — migrate throws. Usesqlite/postgresfor FTS. AUTOINCREMENT. Numeric ids (id({ scheme: 'numeric' })) emit plainINTEGER PRIMARY KEY(still auto-allocating, just without the no-reuse-of-deleted-ids guarantee). The default TypeID/ULID schemes are unaffected.
Everything else — RETURNING, ON CONFLICT upserts, json_object/json_group_array eager-loads, PRAGMA introspection, partial + expression indexes, CHECK constraints, namespace ATTACH (multi-tenant physical isolation) — works identically to sqlite.
DDL runs in autocommit
Turso rejects DDL inside a BEGIN CONCURRENT transaction ("DDL statements require an exclusive transaction"). So voltro migrate / auto-migrate runs each DDL statement in autocommit on turso (sequential, idempotent CREATE … IF NOT EXISTS). A partial failure simply recovers on the next boot; DML transactions still use BEGIN CONCURRENT.
Identifier quoting
"name" — double quotes. Same as postgres + sqlite.
File vs :memory:
:memory:— ephemeral, per-connection, pool clamped to 1. Tests + smoke fixtures.file:./db.turso— durable. Pool of N connections share the one file; MVCC coordinates concurrent writers.
Where it lives
voltro/packages/sql-turso/src/sqlLayer.ts— the URL-scheme ROUTER:connectionFromConfigparses aConnectionConfig(+ token/sync env) into alocal(Rust engine) orremote(libsql) connection and builds the matching layervoltro/packages/sql-turso/src/sqlClient.ts— the LOCAL pooled@effect/sqlclient: async connection, per-connection prepare cache,Poolacquirers,beginTransaction: 'BEGIN'(DDL-safe default; the store upgrades its own DML transactions toBEGIN CONCURRENTper-fiber viaConcurrentTransaction), MVCC pragma,:memory:size-1 clampvoltro/packages/sql-turso/src/libsqlClient.ts— the REMOTE@effect/sqlclient over@libsql/client: autocommitclient.execute, an interactiveclient.transaction('write')fortransactional()(with BEGIN as a no-op and COMMIT/ROLLBACK mapped onto the tx object), and the embedded-replica sync (syncUrl+ initialclient.sync()). The auth token is config/env-sourced and never loggedvoltro/packages/sql-turso/src/retry.ts—isTursoRetryableFailure(matches the"Write-write conflict"/ busy MESSAGE; the local engine tags every error with a generic code, so the retry identity is the message)voltro/packages/sql-turso/src/index.ts—tursoDialect(id: 'turso'); reusesmakeSqliteStorefrom@voltro/sql-sqlitewith the routed client + retry predicatevoltro/packages/sql-sqlite/src/store.ts— the shared SQLite-family store (driver + retry predicate + span name are injected)voltro/packages/database/src/migrate.ts— turso DDL branch (numeric-id without AUTOINCREMENT; generated-column + FTS guards; autocommit DDL)