· typescript / turso / sqlite

How to set up Turso (libSQL) in a TypeScript project

Set up Turso (libSQL) in TypeScript from scratch: install @libsql/client, run CRUD, add Drizzle ORM, and manage local dev — with the gotchas most guides miss.

By

1,753 words · 9 min read

Turso is a hosted SQLite database that works anywhere your TypeScript runs — Node.js, Bun, Cloudflare Workers, Vercel Edge. The free tier covers 100 databases, 500 million row reads, and 5 GB of storage per month. That is enough to run a real app in production without paying anything.

This tutorial takes you from zero to a working Turso setup in TypeScript, with optional Drizzle ORM on top. Every code block here is runnable; none of it is pseudocode.

Who this is for

TypeScript developers who want a lightweight, low-cost SQLite database in the cloud. You should be comfortable with async/await and have Node.js 18+ (or Bun 1+) installed. If you need complex JSON path queries, PostGIS, or very high write throughput, stay on Postgres — see the gotchas section below.

Step 1: Create a Turso database

# Install the Turso CLI (macOS)
brew install tursodatabase/tap/turso

# Authenticate
turso auth login

# Create a database
turso db create my-app-db

# Get the connection URL
turso db show my-app-db

# Create an auth token
turso db tokens create my-app-db

turso db show prints a URL in the form libsql://my-app-db-[org].turso.io. turso db tokens create prints the token. Copy both — you need them in a moment.

Via the web dashboard

Sign up at turso.tech, click New Database, choose a region, and copy the URL and token from the dashboard. The dashboard also shows row read/write counts, storage, and PITR restore points.

Step 2: Install @libsql/client

For Node.js and Bun runtimes, install @libsql/client. For Cloudflare Workers and other edge runtimes, Turso recommends @tursodatabase/serverless instead — see the edge runtime note in Step 3.

npm install @libsql/client
# or
bun add @libsql/client

Store your credentials:

# .env
TURSO_DATABASE_URL=libsql://my-app-db-[org].turso.io
TURSO_AUTH_TOKEN=your-token-here

The env var names matter. Many tutorials use DATABASE_URL or DATABASE_AUTH_TOKEN — both are wrong. Use TURSO_DATABASE_URL and TURSO_AUTH_TOKEN.

Step 3: Connect

The client supports three URL schemes. One package, one createClient call, three modes:

import { createClient } from '@libsql/client';

// 1. In-memory — for testing or scratch work
const client = createClient({ url: ':memory:' });

// 2. Local SQLite file — for development
const client = createClient({ url: 'file:local.db' });
// No authToken needed for file: connections

// 3. Remote Turso Cloud — for production
const client = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

Edge runtimes (Cloudflare Workers, Vercel Edge Functions): Turso recommends the @tursodatabase/serverless package for edge runtimes. Install it separately and import from @tursodatabase/serverless/compat:

npm install @tursodatabase/serverless
// Cloudflare Workers and other edge runtimes
import { createClient } from '@tursodatabase/serverless/compat';

const client = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

The /compat path exposes the same createClient interface as @libsql/client, so the rest of your code stays unchanged. It does not support file: URLs, so your local dev still uses the default @libsql/client import.

Step 4: CRUD with the raw client

The core API is client.execute(). Pass a plain SQL string, or an object with sql and args for parameterized queries. Always parameterize — never interpolate user input into SQL strings.

import { createClient } from '@libsql/client';

const client = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

// Create a table
await client.execute(`
  CREATE TABLE IF NOT EXISTS todos (
    id    INTEGER PRIMARY KEY AUTOINCREMENT,
    text  TEXT NOT NULL,
    done  INTEGER NOT NULL DEFAULT 0
  )
`);

// INSERT
const insert = await client.execute({
  sql: 'INSERT INTO todos (text) VALUES (?)',
  args: ['Buy oat milk'],
});
console.log('new id:', insert.lastInsertRowid);

// SELECT all
const { rows } = await client.execute('SELECT * FROM todos');
console.log(rows);

// SELECT one row
const { rows: [todo] } = await client.execute({
  sql: 'SELECT * FROM todos WHERE id = ?',
  args: [1],
});

// UPDATE
await client.execute({
  sql: 'UPDATE todos SET done = 1 WHERE id = ?',
  args: [1],
});

// DELETE
await client.execute({
  sql: 'DELETE FROM todos WHERE id = ?',
  args: [1],
});

The result shape is { rows, columns, rowsAffected, lastInsertRowid }.

Batch and transactions

Use client.batch() when you want all-or-nothing execution across multiple statements without interactive control:

await client.batch([
  'INSERT INTO users (name) VALUES ("alice")',
  { sql: 'INSERT INTO orders (user_id) VALUES (?)', args: [1] },
], 'write');

Use client.transaction() when you need to commit or roll back based on runtime logic:

const txn = await client.transaction('write');
try {
  await txn.execute('UPDATE accounts SET balance = balance - 100 WHERE id = 1');
  await txn.execute('UPDATE accounts SET balance = balance + 100 WHERE id = 2');
  await txn.commit();
} catch (err) {
  await txn.rollback();
  throw err;
}

Transaction modes: 'write' (full read/write), 'read' (SELECT only), 'deferred' (starts read, escalates on demand).

Drizzle is the most common ORM pairing for Turso. The setup takes five minutes. If you are deciding between Drizzle and Kysely, see Drizzle vs Kysely for a side-by-side comparison of the two most-used typed query tools in TypeScript.

Install

npm install drizzle-orm @libsql/client dotenv
npm install -D drizzle-kit tsx

drizzle.config.ts

import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  out: './drizzle',
  schema: './src/db/schema.ts',
  dialect: 'turso',           // NOT 'sqlite' — 'turso' is required
  dbCredentials: {
    url: process.env.TURSO_DATABASE_URL!,
    authToken: process.env.TURSO_AUTH_TOKEN!,
  },
});

dialect: 'sqlite' is the most common mistake here. It will silently ignore the authToken field. Use 'turso'.

Schema

// src/db/schema.ts
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';

export const usersTable = sqliteTable('users_table', {
  id:    int().primaryKey({ autoIncrement: true }),
  name:  text().notNull(),
  age:   int().notNull(),
  email: text().notNull().unique(),
});

Database connection

// src/db/index.ts
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/libsql';

export const db = drizzle({
  connection: {
    url:       process.env.TURSO_DATABASE_URL!,
    authToken: process.env.TURSO_AUTH_TOKEN!,
  },
});

CRUD with Drizzle

import { eq } from 'drizzle-orm';
import { db } from './db';
import { usersTable } from './db/schema';

// INSERT
await db.insert(usersTable).values({ name: 'Alice', age: 28, email: '[email protected]' });

// SELECT all
const users = await db.select().from(usersTable);

// SELECT where
const [user] = await db
  .select()
  .from(usersTable)
  .where(eq(usersTable.email, '[email protected]'));

// UPDATE
await db
  .update(usersTable)
  .set({ age: 29 })
  .where(eq(usersTable.email, '[email protected]'));

// DELETE
await db
  .delete(usersTable)
  .where(eq(usersTable.email, '[email protected]'));

Migrations

# Generate migration files
npx drizzle-kit generate

# Push schema directly (dev shortcut — no migration files generated)
npx drizzle-kit push

# Apply existing migration files
npx drizzle-kit migrate

Step 6: Local development

Three options, in order of how often you want to reach for them:

Option A — SQLite file (simplest). Add TURSO_DATABASE_URL=file:local.db to .env.local and let your createClient call pick it up. No server, no CLI, works fully offline. This is the right default for most projects.

Option B — turso dev local server. Runs a local libSQL server at http://127.0.0.1:8080. Useful when you need to test libSQL-specific behaviour (embedded replica sync, multi-database queries) that plain SQLite won’t replicate.

# Ephemeral — data is gone when the process stops
turso dev

# Persistent — data survives restarts
turso dev --db-file local.db

The ephemeral mode losing data on stop is the single most common gotcha in Turso’s support channels. Always pass --db-file unless you want a throw-away database.

Point your app at http://127.0.0.1:8080 with any non-empty string as the auth token for local use.

Option C — live remote database. Point dev directly at a non-production Turso Cloud database. Simplest prod parity; not worth the cloud egress cost for routine local work.

Gotchas vs Postgres

Data types

SQLite has fewer native types than Postgres. Here is how to map them:

Postgres typelibSQL equivalent
uuidTEXT — use crypto.randomUUID() in app
jsonbTEXT — parse in app; no JSON path operators
ARRAYNot supported — use a join table or JSON text
ENUMTEXT with CHECK constraint
BOOLEANINTEGER (0/1)
TIMESTAMPTZINTEGER (Unix ms) or TEXT (ISO 8601)

Write throughput

Turso is designed for read-heavy workloads. The remote primary uses a single-writer model, so high-concurrency write workloads — a busy event log, a payment ledger, a multiplayer game state store — should stay on Postgres instead.

Turso’s embedded replica mode makes local reads microsecond-fast. That read speed applies to the local replica only — remote writes still round-trip to the primary.

Missing features

  • No stored procedures or server-side functions
  • No LISTEN/NOTIFY
  • No PostGIS or extension ecosystem
  • No materialized views
  • Limited ALTER TABLE — adding columns is fine; renaming or dropping columns requires recreating the table
  • Full-text search via FTS5 exists but is less capable than Postgres tsvector

What Turso adds over plain SQLite

  • Networked access over HTTP/WebSocket
  • Multi-database per org (100 free, unlimited on paid)
  • Point-in-time restore
  • Native vector search — useful for embedding workloads
  • WAL mode always on (no need to PRAGMA journal_mode=WAL)

Pricing and when to use it

As of June 2026 (turso.tech/pricing):

PlanPriceDatabasesStorageRows Read/moRows Written/mo
Free$01005 GB500M10M
Developer$4.99/moUnlimited9 GB2.5B25M
Scaler$24.92/moUnlimited24 GB100B100M
Pro$416.58/moUnlimited50 GB (+$0.45/GB)250B250M

The free tier’s 500M row reads is more than most read-heavy apps serving a few thousand daily active users will consume in a month.

Choosing between Turso and Cloudflare D1? Our Turso vs D1 comparison covers latency, pricing, and feature tradeoffs in detail.

Use Turso when:

  • You’re building edge-first (Cloudflare Workers, Vercel Edge, Deno Deploy) and want low-latency reads
  • You want per-tenant database isolation — 100 databases free is unusually generous
  • You are on a budget and your data model is simple CRUD without complex Postgres-specific types
  • You want native vector search for AI/embedding workloads

Stay on Postgres when:

  • You need high concurrent write throughput (busy event logs, payment ledgers, multiplayer state)
  • Your schema uses JSONB path queries, PostGIS, custom domains, or logical replication
  • Your team is deep in the Postgres ecosystem and migration cost outweighs the edge latency benefit

References