Integrationsstorage
Turso and libSQL storage
Keep Mayura's workflows, jobs, memory, budgets and API keys in Turso, a sqld server or a local libSQL file with @mayurajs/storage-libsql.
- Package
- @mayurajs/storage-libsql
- Install
- npm install mayura @mayurajs/storage-libsql
- Vendor
- Turso ↗
Turso hosts libSQL, the open-source fork of SQLite. @mayurajs/storage-libsql is a Mayura
store on libSQL: a Turso database, your own sqld server, or a local file. It holds everything
the built-in SQLite and PostgreSQL stores hold: run records and their events (the server's submission journal and run
records), durable workflows with their graphs, trees, waits and discovery, the leased job scheduler, durable budgets,
native memory and API keys. It talks to libSQL through the official
@libsql/client, pinned at 0.18.0, its only dependency. It uses SQLite's SQL and schema, so a local libSQL file and a
mayura/storage-sqlite file are interchangeable. Node.js 22.12 or later, or 24.14.1 or later, is supported; on another
runtime, pass a client you create from @libsql/client/web.
Install
npm install mayura @mayurajs/storage-libsqlCredentials
A Turso database has a URL (libsql://<db>-<org>.turso.io) and a database auth token. Create the database and its
token in the Turso dashboard, or with turso db tokens create <db>. The token needs to read
and write: the store creates its tables and writes to them. A local file and a local sqld without authentication need
no token.
The package reads nothing from the environment: pass the URL and token yourself, for example from
TURSO_DATABASE_URL and TURSO_AUTH_TOKEN. A token in the URL's query string is refused, so it never lands in logs.
Use it
import { createKeyManager } from 'mayura/keys';
import { createNativeMemory } from 'mayura/memory';
import { createLibsqlStore } from '@mayurajs/storage-libsql';
const store = createLibsqlStore({
url: process.env.TURSO_DATABASE_URL ?? '',
authToken: process.env.TURSO_AUTH_TOKEN ?? '',
});
await store.initialize(); // creates Mayura's tables when they are missing
await store.memory.initialize(); // native memory keeps its own tables
const memory = createNativeMemory({
store,
scope: { principalId: 'customer-42', projectId: 'support' },
permissions: { allow: ['memory:read', 'memory:write'] },
});
const keys = createKeyManager({ store, prefix: 'acme' });
// Hand `store` to workflow runtimes, durable budgets and your server too; close it when the process shuts down.
await store.close();For local development, use a file: createLibsqlStore({ url: 'file:./data/app.db' }). The directory must exist.
To use a client you create yourself, for example one from @libsql/client/web on an edge runtime, pass it as
client. The store never closes a client you gave it.
import { createClient } from '@libsql/client/web';
import { createLibsqlStore } from '@mayurajs/storage-libsql';
const client = createClient({ url: 'libsql://my-app-acme.turso.io', authToken: process.env.TURSO_AUTH_TOKEN ?? '' });
const store = createLibsqlStore({ client });
await store.initialize();Options
| Option | Default | Meaning |
|---|---|---|
url |
required, or client |
libsql://, https:// or wss:// for a remote database; http:// or ws:// only on a loopback address (localhost, 127.0.0.1, [::1]); file: or :memory: for a local database. No user name, password or fragment, and no token in the query string. |
authToken |
none | The remote database's token. Give it with url; with client, give the token to the client instead. |
client |
none | A client from @libsql/client you create and own, instead of url. store.close() never closes it. |
Any other option, or both url and client, fails with INVALID_INPUT when you call createLibsqlStore().
Setup and migrations
initialize() creates Mayura's tables when they are missing and checks the schema version. Mayura 1.x uses version 1;
a database at any other version is refused. Run it once per store, before anything else uses the store. Native memory,
durable budgets and the scheduler each have their own initialize() too (see
What uses the store). Releases that change the layout ship a migration you
run as its own step; see Migrations.
A local file keeps SQLite's durability settings: initialize() turns on write-ahead logging and a full sync on every
commit, and fails if write-ahead logging cannot be enabled. Foreign keys are enforced. A remote server's durability is
the server's to configure.
Transactions and concurrency
- Every operation is one write transaction, so a record, its events and the scheduler state it touches change together or not at all.
- One store runs its write transactions one at a time, with up to 256 waiting. Beyond that a call fails with
QUEUE_FULL; retry with backoff. - When another process holds a local file's write lock, the store retries the start of its transaction for up to 5
seconds, without blocking the event loop. On a remote server the writer waits on the server's side instead:
sqldwaits up to 5 seconds, then rolls back the transaction holding the lock. - Integers are read in libSQL's
numbermode, so a value beyond 2^53 fails instead of being rounded. - Identifiers are compared byte for byte: keys that differ only in case or accents are different keys.
Errors
Failures reach you as a StorageError with a storageCode; messages never contain the driver's text, SQL or your
token. See Errors for every code.
storageCode |
When |
|---|---|
INVALID_INPUT |
An option is missing, unknown or malformed, such as an http:// URL to another host or a token in the URL. |
CONFLICT |
A record changed after it was read, or an id or idempotency key already holds other content (a SQLite constraint violation). |
QUEUE_FULL |
256 operations are already waiting for this store. |
STORAGE_UNAVAILABLE |
The database stayed locked by another writer, could not be reached, refused the request, or holds an unsupported schema version. |
STORE_NOT_INITIALIZED |
A call came before await store.initialize(). |
STORE_CLOSED |
A call came after close(). |
How it is tested
- The storage, memory and workflow conformance suites that the SQLite and PostgreSQL stores pass run on a local libSQL file: records, the key manager, identity integrity, scheduled bounds, memory and native memory, workflows, the scheduler, execution waits, durable budgets, scheduled workflows, graphs and graph discovery, with crash tests that kill a child process mid-operation and a check that the SQLite adapter can open the same file.
- In CI, with
MAYURA_TEST_LIBSQL_URLset, the suites run again against a realsqldserver (the libSQL server Turso runs) over the network: records, identity integrity, scheduled bounds, memory and native memory, workflows, the scheduler, execution waits and durable budgets, plus the workflow tree suites and a check that foreign keys hold inside the server's transactions. - Unit tests check the durability settings, the URL and token rules, that a client you give is never closed, and that a store waits out another process's write lock.
scripts/live/storage-libsql.mjsran on Turso on October 6, 2026 and passed: it migrated the database, wrote a record and its event and read them back, and issued, verified and revoked an API key.
Related
- Storage: every store, migrations, backups and errors.
- MySQL and Cloudflare D1: other databases for the same store.
- Authentication: API keys kept in this store.
- Durable workflows: what runs on the store.