Turso Tutorial: Getting Started with Edge SQLite (libSQL)

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.

What Turso actually is #

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.

Install the CLI and authenticate #

The CLI is the fastest path to a database. Install it, then log in through the browser flow.

bash.bash

# 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 and grab credentials #

Create a database, then pull the two things every client needs: a connection URL and an auth token.

bash.bash

# 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.

Connect from a Node/TypeScript app #

Install the client and run a create-table, insert, and query in one file. This is the whole loop.

typescript.typescript

// 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.

Embedded replicas: the headline feature #

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.

typescript.typescript

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.

Edge replication and locations #

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:

bash.bash

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.

Schema, migrations, and local dev #

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.

Pricing and limits at a glance #

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.

Gotchas and when not to use it #

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.

Next steps #

Wire an embedded replica into a real API route and measure read latency before and after.

Add Drizzle or Prisma and run a migration against your primary.

Create per-tenant databases with turso db create and script token issuance.

Test a read-your-writes path and decide where sync() belongs.

The call we'd make #

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.