A hands-on walkthrough of Turso and libSQL, from CLI setup to embedded replicas that put SQLite reads next to your users.
Turso is a managed database service built on libSQL, an open-source fork of SQLite that adds replication, a remote network protocol, and an embedded-replica sync engine. If you have ever wanted the ergonomics of a single-file SQLite database with the reach of a distributed system, this is the closest thing on the market. This tutorial gets you from zero to a working app, and it flags the places where the edge model bites back.
For the wider context of where this fits, see our serverless and edge databases guide.
SQLite runs in-process, which is fantastic for latency and terrible for sharing state across servers. libSQL keeps the SQLite storage format and query engine, then bolts on a server (sqld) that speaks HTTP and WebSocket, plus a sync protocol so a local SQLite file can pull changes from a remote primary. Turso hosts that infrastructure for you: a primary database in one region, optional read replicas in others, and auth tokens to gate access.
The mental model that matters: one primary handles writes, and many replicas serve reads. Everything else follows from that.
The CLI is the fastest path to a database. Install it, then log in through the browser flow.
# macOS / Linux
curl -sSfL https://get.tur.so/install.sh | bash
# Authenticate (opens a browser)
turso auth signup # or: turso auth login
# Confirm you are authenticated
turso auth whoami
Install methods and command names shift over time, so verify against current Turso/libSQL docs if a step looks different from what you see here.
Create a database, then pull the two things every client needs: a connection URL and an auth token.
# Create a database (Turso picks a nearby primary location by default)
turso db create edge-demo
# The libsql:// connection URL
turso db show edge-demo --url
# A scoped auth token for the client
turso db tokens create edge-demo
# Open an interactive SQL shell to sanity-check
turso db shell edge-demo
Store the URL and token as environment variables. Never commit the token; it is a bearer credential with full access to whatever scope you granted it.
Install the client and run a create-table, insert, and query in one file. This is the whole loop.
// db.ts
import { createClient } from "@libsql/client";
const db = createClient({
url: process.env.TURSO_DATABASE_URL!, // libsql://edge-demo-<org>.turso.io
authToken: process.env.TURSO_AUTH_TOKEN!,
});
async function main() {
await db.execute(`
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY,
body TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
)
`);
// Always use bound parameters, never string interpolation
await db.execute({
sql: "INSERT INTO notes (body) VALUES (?)",
args: ["First note from the edge"],
});
const result = await db.execute("SELECT id, body, created_at FROM notes");
console.table(result.rows);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Run it with TURSO_DATABASE_URL=... TURSO_AUTH_TOKEN=... npx tsx db.ts. Every execute here is a round trip to the primary over HTTP. That is fine for a script, but for a read-heavy app you want replicas closer to the query.
An embedded replica is a real local SQLite file that libSQL keeps in sync with the primary. Reads hit the local file at in-process speed; writes are forwarded to the primary and then pulled back down. You get near-zero read latency without giving up a single source of truth.
import { createClient } from "@libsql/client";
const db = createClient({
url: "file:local.db", // local SQLite file
syncUrl: process.env.TURSO_DATABASE_URL!, // remote primary
authToken: process.env.TURSO_AUTH_TOKEN!,
syncInterval: 60, // background sync, seconds
});
// Pull the latest from the primary before a read-critical path
await db.sync();
const rows = await db.execute("SELECT count(*) AS n FROM notes");
console.log(rows.rows[0].n);
Reads now resolve against local.db. Writes still travel to the primary, and sync() reconciles the local copy. You choose the freshness tradeoff: sync on an interval, sync on demand, or both.
Beyond embedded replicas, Turso can place managed read replicas in multiple regions. A client connecting over libsql:// is routed to the nearest replica for reads, while writes are proxied to the primary. You control which locations exist:
turso db locations list
turso db replicate edge-demo <location-code>
This is the same primary-write, replica-read pattern operating at the platform level instead of inside your process. For a deeper comparison of this model against another popular option, read Turso vs Cloudflare D1.
Turso does not ship an opinionated migration framework, and that is a feature. Because it is SQLite underneath, tools like Drizzle Kit, Prisma, and Atlas work through the libSQL driver. Keep migrations as ordered SQL files and apply them against the primary; replicas inherit the schema through normal replication.
For local development, you have two clean options. Point createClient at a plain file:dev.db and skip the network entirely, or run turso dev to start a local sqld server that mimics the remote protocol. The first is faster; the second catches protocol-specific behavior before it reaches production.
Turso has a free tier generous enough for side projects and prototypes, with paid tiers scaling on storage, row reads/writes, and number of databases. The "database per tenant" pattern is a genuine strength here because databases are cheap to create. Exact quotas change, so check current pricing before you architect around a specific limit.
Eventual consistency: Replicas and embedded copies lag the primary. A user who writes and immediately reads from a replica may not see their own change. Read-your-writes needs care, either by reading from the primary on that path or by calling sync() first.
Write centralization: All writes funnel to one primary. If your workload is write-heavy or write-latency-sensitive from far-away regions, the edge advantage shrinks. Turso optimizes reads, not global writes.
Single-region write bottleneck: A primary in us-east means European writes cross the Atlantic every time. Place the primary where writes originate, not where the most users read.
Skip Turso when you need strong global write consistency, heavy multi-statement transactions across regions, or relational features SQLite lacks. For those, a Postgres-class system is the honest choice.
turso db create and script token issuance.sync() belongs.For read-heavy apps that want SQLite simplicity with global reach, Turso is a strong default, and embedded replicas are the reason. Start with a single primary near your writers, add replicas or embedded copies where reads cluster, and treat eventual consistency as a design constraint from day one rather than a surprise in production. If your app is write-heavy or needs cross-region transactional guarantees, reach for a different tool. For everything in between, this is a pragmatic, low-friction way to put your data next to your users.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Explore more articles in this category
A practical look at when SQLite is a genuinely good production database, where it breaks, and the tooling that makes it viable.
A practitioner's guide to choosing between serverless and provisioned databases based on cost, latency, connections, and load shape.
The database is going serverless and moving to the edge. This is the map: what these terms actually mean, when they win, and how the options compare.
Evergreen posts worth revisiting.