libSQL and Turso skill for Codex
Local-speed reads with a remote SQLite primary. Codex chooses the access mode from where the code runs: embedded replicas on servers with a disk, HTTP clients in serverless and edge runtimes. It states the freshness contract (read-your-writes locally, other instances after the sync interval), batches writes to save round trips, keeps tokens scoped and out of bundles, and fans migrations out safely when each tenant has its own database.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- libsql-turso Installs to ~/.agents/skills/libsql-turso/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install libsql-turso The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project libsql-turso Installs to .agents/skills/libsql-turso/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/libsql-turso.zipunzip -o libsql-turso.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) libsql-turso PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $libsql-turso, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds and operates apps on Turso and libSQL, the SQLite fork - remote databases over libsql:// URLs, embedded replicas that read from a local file and sync from the primary, database-per-tenant layouts, the turso CLI, auth tokens, batches and interactive transactions in @libsql/client, Drizzle and Prisma adapters, and libSQL vector search. Use when wiring @libsql/client or the Rust/Go/Python libSQL SDKs, adding embedded replicas for low-latency reads, choosing between edge HTTP access and a local replica, or debugging stale reads, sync errors and token problems.
Use it when
- The app uses @libsql/client or another libSQL SDK
- Reads need to be local while writes go to a shared primary
- Picking between embedded replicas and remote HTTP access per runtime
- Stale reads, sync failures or token errors appear with Turso
Use something else when
- Plain single-node SQLite with WAL and Litestream: use sqlite-production
- The app runs on Cloudflare Workers with D1: use cloudflare-d1
How to check the result
Write on one instance and confirm it reads back immediately there and on a second instance within the sync interval; with the primary unreachable, reads still succeed from the replica.
The mistake it prevents
Opening the embedded replica file from a second process or a shared volume while it syncs, which the Turso docs warn can corrupt it.
Done well means: every read path knows whether it hits a local replica or the remote primary, writes always reach the primary, staleness after other clients’ writes is a documented, bounded number of seconds, and tokens are scoped and never shipped to a browser.
Orient first
- Read
AGENTS.md,package.json(orCargo.toml,go.mod) and the DB client module. Note which SDK is in use:@libsql/client(TypeScript),libsqlcrate (Rust),go-libsql/libsql-client-go(Go),libsql(Python). - Note where the code runs: long-lived server or VM with a disk, serverless function, or edge runtime with no filesystem. That decides the access mode.
- Find how migrations run (drizzle-kit, Prisma, plain SQL files) and against which URL.
Turso changes quickly. Before planning around a platform feature, check the current docs (needs network): Turso discontinued hosted edge replicas and multi-db schemas for new users in January 2025, and now promotes a separate Turso Database engine with its own “Turso Sync” (push/pull) for new local-first projects. Embedded replicas in the libSQL SDKs remain supported.
Pick the access mode
| Runtime | Mode | Client setup |
|---|---|---|
| Server, VM, container with a writable disk | Embedded replica | url: "file:..." + syncUrl + authToken |
| Serverless functions (Lambda, Vercel) | Remote over HTTP | url: "libsql://..." + authToken |
| Edge runtimes (Workers, Vercel Edge) | Remote, web client | import { createClient } from "@libsql/client/web" |
| Local development and tests | Local file or turso dev | url: "file:local.db" or http://127.0.0.1:8080 |
| Mobile/desktop app with offline writes | Evaluate Turso Sync or the offline option | Read current docs first |
CLI essentials
turso auth login # interactive, needs network
turso db create app-prod --group default
turso db show app-prod --url # libsql://app-prod-<org>.turso.io
turso db tokens create app-prod --expiration 30d # full-access token
turso db tokens create app-prod -r # read-only token
turso db shell app-prod # SQL shell against the primary
turso dev --db-file local.db # local libSQL server on :8080
Store tokens in the secret manager or .env (gitignored). Tokens grant database access; a
token in client-side JavaScript is a public credential.
Embedded replicas
import { createClient } from "@libsql/client";
export const db = createClient({
url: "file:/var/data/replica.db", // local SQLite file: reads are served here
syncUrl: process.env.TURSO_DATABASE_URL, // libsql://... primary
authToken: process.env.TURSO_AUTH_TOKEN,
syncInterval: 60, // seconds between background syncs
});
await db.sync(); // first sync at startup before serving reads
How it behaves:
- Reads are local SQLite reads (microseconds), so N+1 patterns hurt far less than over HTTP.
- Writes are forwarded to the primary. After a successful write, the same replica sees its own write immediately (read-your-writes). Other replicas see it after their next sync.
- Staleness for other writers is up to
syncIntervalplus sync time. Calldb.sync()explicitly after a webhook or before a read that must be fresh. - Sync works in WAL frames of 4 KB; a one-byte change still ships a full frame.
- Deleting the local file forces a full resync from the primary on next start.
Rules:
- One client owns one replica file. Do not open the file with another SQLite connection or process while it syncs; the docs warn this can corrupt it.
- On platforms with ephemeral disks (most containers), the replica rebuilds on every deploy; budget for that first sync on large databases or mount a volume.
- Embedded replicas need a filesystem; they do not work in edge runtimes.
Rust equivalent:
let db = libsql::Builder::new_remote_replica("replica.db", url, token)
.sync_interval(std::time::Duration::from_secs(60))
.build()
.await?;
db.sync().await?;
let conn = db.connect()?;
Queries, batches and transactions
const rs = await db.execute({
sql: "SELECT id, email FROM users WHERE org_id = ?",
args: [orgId],
});
rs.rows.forEach((r) => console.log(r.id, r.email));
// Atomic batch: one round trip, all-or-nothing
await db.batch([
{ sql: "INSERT INTO orders (id, user_id) VALUES (?, ?)", args: [id, userId] },
{ sql: "UPDATE users SET order_count = order_count + 1 WHERE id = ?", args: [userId] },
], "write");
// Interactive transaction: holds a connection; keep it short
const tx = await db.transaction("write");
try {
const { rows } = await tx.execute({ sql: "SELECT balance FROM accounts WHERE id = ?", args: [a] });
if (Number(rows[0].balance) < amount) throw new Error("insufficient funds");
await tx.execute({ sql: "UPDATE accounts SET balance = balance - ? WHERE id = ?", args: [amount, a] });
await tx.commit();
} finally {
tx.close();
}
| Need | Use |
|---|---|
| Several writes, no reads in between | batch(..., "write") |
| Read then decide then write | transaction("write"), as short as possible |
| Read-only consistent snapshot | batch(..., "read") or transaction("read") |
| Schema migration file | executeMultiple(sql) (not atomic) or a migration tool |
Over remote HTTP, each execute is a network round trip. Batch, or join in SQL, rather
than looping queries. SQLite allows one writer at a time per database: long write
transactions block other writers.
intMode defaults to "number"; set intMode: "bigint" when ids or counters can exceed
2^53.
ORMs
// Drizzle
import { drizzle } from "drizzle-orm/libsql";
export const orm = drizzle(db);
// drizzle.config.ts: dialect: "turso", dbCredentials: { url, authToken }
Prisma uses the @prisma/adapter-libsql driver adapter. Prisma Migrate does not apply
migrations to Turso directly: generate SQL with prisma migrate diff and apply it with
turso db shell <db> < migration.sql, or move migrations to a tool that supports libSQL.
Run migrations against the primary URL, never against a replica file.
Database per tenant
Turso makes many small databases cheap, which suits strict tenant isolation:
- Create databases through the Platform API from a provisioning job, in a group whose location matches the tenant’s region.
- Keep a registry table (tenant id to database name) in a control database.
- Migrations must fan out to every tenant database. Track the applied version per database and make the runner resumable; one failed tenant must not block the rest.
- Use group-scoped tokens so the app does not hold one token per tenant.
For choosing between this and shared-schema tenancy, read $multi-tenancy.
Vector search
libSQL has native vectors without an extension:
CREATE TABLE docs (id INTEGER PRIMARY KEY, body TEXT, embedding F32_BLOB(768));
CREATE INDEX docs_vec_idx ON docs (libsql_vector_idx(embedding));
INSERT INTO docs (body, embedding) VALUES (?, vector32(?)); -- arg: '[0.1, 0.2, ...]'
SELECT d.id, d.body
FROM vector_top_k('docs_vec_idx', vector32(?), 10) AS v
JOIN docs d ON d.rowid = v.id;
Dimensions in F32_BLOB(n) must match the embedding model exactly.
Verification
- Startup logs show the first
sync()completing before traffic is accepted - Write on replica A, read on A immediately (visible), read on B after
syncInterval - Kill the network to the primary: reads continue from the replica, writes fail with a clear error rather than hanging (set request timeouts)
- Tokens: read-only token used where only reads happen; none in client bundles
- Migrations applied once to the primary;
PRAGMA table_info(<table>)on a replica after sync shows the new schema
Gotchas
file:URLs are relative to the process working directory; use absolute paths in services.- Two app instances must each have their own replica file; a shared network volume with one file is the corruption scenario the docs warn about.
@libsql/clientin Next.js middleware or edge routes must import@libsql/client/web; the default entry needs Node APIs.- SQLite type affinity still applies: a column declared
INTEGERaccepts text unless the table isSTRICT. UseCREATE TABLE ... STRICTfor new tables. - Foreign keys are enforced only with
PRAGMA foreign_keys = ONon the connection; check what your client and the remote server do before relying on cascades.
Output template
**Runtime:** Fly.io machines (persistent volume) -> embedded replicas
**Client:** @libsql/client, replica /data/replica.db, syncInterval 30 s, sync on boot
**Writes:** forwarded to libsql://app-prod-acme.turso.io; read-your-writes per instance
**Freshness contract:** other instances <= 30 s; checkout flow calls db.sync() first
**Tokens:** app full-access (30 d rotation), analytics read-only
**Checked:** cross-instance visibility test, primary-outage read test, no token in bundle What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "libSQL and Turso"
short_description: "Local-speed reads with a remote SQLite primary"
default_prompt: "$libsql-turso Add a Turso embedded replica to our Fly.io app so reads stop going over the network" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.