Turso Database 2026: Edge SQLite Setup, Costs & Pitfalls
Set up Turso's edge SQLite in minutes, wire up Drizzle, and learn where embedded replicas break down — plus migration notes and when to pick Postgres.

Table of contents
- Turso Database Guide 2026: The Edge Database Revolution
- What is Turso?
- Key Features
- How Turso Works
- Getting Started with Turso
- Installation
- Authentication
- Creating Your First Database
- Using the Turso Shell
- Connecting from Your Application
- JavaScript/TypeScript (libSQL Client)
- Using with Drizzle ORM
- Embedded Replicas: Zero-Latency Reads
- When to Use Embedded Replicas
- Database Branching
- Branching Workflow
- Global Replication
- Adding Replicas
- Choosing Locations
- Automatic Location Selection
- Real-World Implementation: Contact Form
- Performance Optimization
- Connection Pooling
- Indexing
- Query Optimization
- The Edge Reality Check: Replication, Writes, and When Not to Use Turso
- Reads go to the edge. Writes do not.
- Embedded replicas have a cold-start bill
- When not to use Turso
- Migration notes from PostgreSQL or MySQL
- Turso vs Traditional Databases
- When to Choose Turso
- Pricing
- Free Tier (Starter)
- Scaler ($29/month)
- Enterprise
- Common Patterns
- Soft Deletes
- Audit Logging
- Full-Text Search
- Why We Use Turso at Codebrand
- How We Can Help
- Need help building this?
Turso Database Guide 2026: The Edge Database Revolution
Traditional databases have a problem: they live in one place. Your users are everywhere. Every database query travels across the globe, adding latency that modern users won’t tolerate.
Turso solves this by bringing your database to the edge—replicating data across 35+ locations worldwide. The result? Database queries that complete in single-digit milliseconds, no matter where your users are.
After implementing Turso in production projects, we’ve seen dramatic improvements in application performance. This guide shows you everything you need to know—including the parts the marketing pages skip: replication lag, the single-writer primary, and the workloads where Turso is the wrong answer.
Last reviewed July 2026 against the current libSQL client and Drizzle Kit releases.
What is Turso?
Turso is an edge-hosted, distributed database built on libSQL, an open-source fork of SQLite. It combines SQLite’s simplicity and reliability with global replication and modern cloud infrastructure.
Key Features
- Global Replication: Data replicated across 35+ edge locations
- SQLite Compatible: Use familiar SQL syntax and tooling
- Embedded Replicas: Local SQLite replicas for zero-latency reads
- Branching: Create database branches for testing and development
- Serverless: No server management, automatic scaling
- Low Latency: Single-digit millisecond queries from edge locations
How Turso Works
Traditional Database:
User (Tokyo) → Database (Virginia) → Response
Latency: 150-300ms
Turso Edge Database:
User (Tokyo) → Edge Replica (Tokyo) → Response
Latency: 5-20ms
Turso maintains a primary database and automatically replicates data to edge locations. Reads are served from the nearest replica; writes go to the primary and propagate globally.
Getting Started with Turso
Installation
# Install Turso CLI
# macOS
brew install tursodatabase/tap/turso
# Linux
curl -sSfL https://get.tur.so/install.sh | bash
# Windows (WSL)
curl -sSfL https://get.tur.so/install.sh | bash
Authentication
# Login (opens browser for authentication)
turso auth login
# Check authentication status
turso auth status
Creating Your First Database
# Create a database
turso db create my-app-db
# Create in a specific region
turso db create my-app-db --location ord # Chicago
# List your databases
turso db list
# Get database URL
turso db show my-app-db --url
# Get authentication token
turso db tokens create my-app-db
Using the Turso Shell
# Open interactive shell
turso db shell my-app-db
# Execute SQL
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (email, name) VALUES ('john@example.com', 'John Doe');
SELECT * FROM users;
# Exit shell
.quit
Connecting from Your Application
JavaScript/TypeScript (libSQL Client)
npm install @libsql/client
// db.ts
import { createClient } from "@libsql/client";
const db = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
// Simple query
const result = await db.execute("SELECT * FROM users");
console.log(result.rows);
// Parameterized query (prevents SQL injection)
const user = await db.execute({
sql: "SELECT * FROM users WHERE email = ?",
args: ["john@example.com"],
});
// Insert with returning
const newUser = await db.execute({
sql: "INSERT INTO users (email, name) VALUES (?, ?) RETURNING *",
args: ["jane@example.com", "Jane Doe"],
});
// Transaction
await db.batch([
{
sql: "INSERT INTO orders (user_id, total) VALUES (?, ?)",
args: [1, 99.99],
},
{
sql: "UPDATE users SET order_count = order_count + 1 WHERE id = ?",
args: [1],
},
]);
Using with Drizzle ORM
Drizzle is our recommended ORM for Turso—type-safe, performant, and excellent developer experience.
npm install drizzle-orm @libsql/client
npm install -D drizzle-kit
Define Your Schema:
// src/db/schema.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
email: text("email").unique().notNull(),
name: text("name").notNull(),
role: text("role", { enum: ["user", "admin"] }).default("user"),
createdAt: text("created_at").default("CURRENT_TIMESTAMP"),
});
export const posts = sqliteTable("posts", {
id: integer("id").primaryKey({ autoIncrement: true }),
title: text("title").notNull(),
content: text("content").notNull(),
authorId: integer("author_id").references(() => users.id),
published: integer("published", { mode: "boolean" }).default(false),
createdAt: text("created_at").default("CURRENT_TIMESTAMP"),
});
Initialize Drizzle:
// src/db/index.ts
import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client";
import * as schema from "./schema";
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
export const db = drizzle(client, { schema });
Query with Full Type Safety:
import { db } from "./db";
import { users, posts } from "./db/schema";
import { eq, desc, and } from "drizzle-orm";
// Select all users
const allUsers = await db.select().from(users);
// Select with conditions
const admins = await db
.select()
.from(users)
.where(eq(users.role, "admin"));
// Insert
const newUser = await db
.insert(users)
.values({
email: "new@example.com",
name: "New User",
})
.returning();
// Update
await db
.update(users)
.set({ role: "admin" })
.where(eq(users.email, "john@example.com"));
// Delete
await db.delete(users).where(eq(users.id, 5));
// Join queries
const postsWithAuthors = await db
.select({
postTitle: posts.title,
authorName: users.name,
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt));
Drizzle Kit Configuration:
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "turso",
dbCredentials: {
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
},
});
Note the dialect key. Older tutorials still show driver: "turso" with a plain satisfies Config export—that shape was retired in Drizzle Kit and will fail on any current version. The dialect-based defineConfig is what you want.
Run Migrations:
# Generate migration
npx drizzle-kit generate
# Push changes directly (development)
npx drizzle-kit push
# View database in Drizzle Studio
npx drizzle-kit studio
The old generate:sqlite / push:sqlite command pairs were removed too—if you copied them from a 2024 blog post, drop the suffix.
Embedded Replicas: Zero-Latency Reads
Turso’s killer feature is embedded replicas—a local SQLite file that syncs with your Turso database. Reads are instant; writes sync automatically.
import { createClient } from "@libsql/client";
const db = createClient({
url: "file:local-replica.db",
syncUrl: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
// Sync replica with remote
await db.sync();
// Reads are now instant (local SQLite)
const users = await db.execute("SELECT * FROM users");
// Writes go to primary and sync
await db.execute({
sql: "INSERT INTO users (email, name) VALUES (?, ?)",
args: ["local@example.com", "Local User"],
});
// Sync again to get latest changes
await db.sync();
When to Use Embedded Replicas
| Scenario | Use Remote | Use Embedded Replica |
|---|---|---|
| Serverless functions | Yes | No — no persistent storage |
| Long-running servers | Yes | Yes |
| Desktop apps | No | Yes |
| Mobile apps | No | Yes |
| Read-heavy workloads | Yes | Yes, and faster |
| Edge workers | Yes | Depends on platform |
Database Branching
Turso lets you create database branches—perfect for testing, staging, and development.
# Create a branch from production
turso db create my-app-staging --from-db my-app-production
# List branches
turso db list
# Use branch in development
export TURSO_DATABASE_URL=$(turso db show my-app-staging --url)
Branching Workflow
Production Database
│
├── Staging Branch (for QA)
│
├── Feature Branch (for development)
│
└── PR Branch (for pull request testing)
Each branch is a full copy of the database at the time of branching. Changes don’t affect the parent.
Global Replication
Adding Replicas
# List available locations
turso db locations
# Add replica location
turso db replicate my-app-db nrt # Tokyo
turso db replicate my-app-db lhr # London
turso db replicate my-app-db syd # Sydney
# Check replication status
turso db show my-app-db
Choosing Locations
Place replicas where your users are:
| Region | Code | Location |
|---|---|---|
| Americas | ord | Chicago |
| Americas | dfw | Dallas |
| Americas | sea | Seattle |
| Americas | gru | São Paulo |
| Europe | lhr | London |
| Europe | ams | Amsterdam |
| Europe | fra | Frankfurt |
| Asia Pacific | nrt | Tokyo |
| Asia Pacific | sin | Singapore |
| Asia Pacific | syd | Sydney |
Automatic Location Selection
The libSQL client automatically routes to the nearest replica:
const db = createClient({
url: process.env.TURSO_DATABASE_URL!, // Routes to nearest
authToken: process.env.TURSO_AUTH_TOKEN!,
});
Real-World Implementation: Contact Form
Here’s a complete example using Turso with Astro and Drizzle:
Schema:
// src/db/schema.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const contacts = sqliteTable("contacts", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
email: text("email").notNull(),
company: text("company"),
message: text("message").notNull(),
status: text("status", {
enum: ["new", "read", "replied", "archived"]
}).default("new"),
createdAt: text("created_at").default("CURRENT_TIMESTAMP"),
});
API Endpoint (Astro):
// src/pages/api/contact.ts
import type { APIRoute } from "astro";
import { db } from "../../db";
import { contacts } from "../../db/schema";
export const POST: APIRoute = async ({ request }) => {
try {
const data = await request.json();
const { name, email, company, message } = data;
// Validate
if (!name || !email || !message) {
return new Response(
JSON.stringify({ error: "Missing required fields" }),
{ status: 400 }
);
}
// Insert into Turso
const result = await db
.insert(contacts)
.values({ name, email, company, message })
.returning();
// Send notification email (optional)
// await sendNotificationEmail(result[0]);
return new Response(
JSON.stringify({ success: true, id: result[0].id }),
{ status: 200 }
);
} catch (error) {
console.error("Contact form error:", error);
return new Response(
JSON.stringify({ error: "Internal server error" }),
{ status: 500 }
);
}
};
Admin Dashboard Query:
// Get all contacts with pagination
import { desc } from "drizzle-orm";
const page = 1;
const limit = 20;
const allContacts = await db
.select()
.from(contacts)
.orderBy(desc(contacts.createdAt))
.limit(limit)
.offset((page - 1) * limit);
// Get unread count
const unreadCount = await db
.select({ count: sql`count(*)` })
.from(contacts)
.where(eq(contacts.status, "new"));
Performance Optimization
Connection Pooling
For serverless environments, reuse connections:
// db.ts - Singleton pattern
let db: ReturnType<typeof drizzle> | null = null;
export function getDb() {
if (!db) {
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
db = drizzle(client);
}
return db;
}
Indexing
Add indexes for frequently queried columns:
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_posts_published ON posts(published, created_at);
With Drizzle:
import { index } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: integer("id").primaryKey(),
email: text("email").unique().notNull(),
name: text("name").notNull(),
}, (table) => ({
emailIdx: index("idx_users_email").on(table.email),
}));
Query Optimization
// Bad: Select all columns
const users = await db.select().from(users);
// Good: Select only needed columns
const users = await db
.select({ id: users.id, name: users.name })
.from(users);
// Bad: N+1 queries
for (const user of users) {
const posts = await db.select().from(posts).where(eq(posts.authorId, user.id));
}
// Good: Single join query
const usersWithPosts = await db
.select()
.from(users)
.leftJoin(posts, eq(users.id, posts.authorId));
The Edge Reality Check: Replication, Writes, and When Not to Use Turso
Everything above is the happy path. Here is what production actually teaches you.
Reads go to the edge. Writes do not.
Turso replicates reads globally, but exactly one primary accepts writes. A user in Sydney reading from the Sydney replica gets single-digit latency. That same user submitting a form still pays the full round trip to your primary region—so a write-heavy app with a US primary and Asian users can feel slower, not faster, than a single regional Postgres sitting next to its users. Put the primary where the writes are, not where your team is.
Replication is also asynchronous. A row written to the primary is not instantly visible on every replica, so read-after-write can return stale data. If a user updates their profile and the next page load hits a lagging replica, they see the old value and file a bug. The fix is architectural: route reads that must be fresh to the primary connection, and let everything else—catalogs, listings, docs, marketing content—come from the edge.
Embedded replicas have a cold-start bill
db.sync() is not free. The first sync on a fresh machine pulls the whole database down; every sync after that transfers changed frames. On a long-running server that amortizes to nothing. On a container recycled every few minutes, you pay a full bootstrap over and over—and you are billed for those reads. Sync on an interval or after known write events, never once per request.
Embedded replicas also need a writable, persistent filesystem. Most serverless runtimes give you neither.
When not to use Turso
- Write-heavy, single-region workloads. SQLite serializes writers. Thousands of concurrent writes per second is a Postgres problem, not an edge problem.
- You depend on Postgres-specific features. JSONB operators, PostGIS, materialized views,
LISTEN/NOTIFY, the extension ecosystem. FTS5 covers search well; it does not cover the rest. - Long analytical transactions. SQLite’s write lock does not tolerate a report that holds a transaction open for a minute.
- Strict data residency requirements. Replicating rows to 35 locations is a feature right up until a regulator asks where the personal data physically lives.
Migration notes from PostgreSQL or MySQL
Budget your time for the type system, not the data transfer. SQLite has no native boolean, no native date/time type, no ENUM, and no sequences. Drizzle papers over most of that with integer({ mode: "boolean" }) and text timestamps, but every raw query, every reporting job, and every downstream integration written against the old schema needs revisiting. UUID columns become TEXT. Foreign key enforcement must be switched on explicitly. Anything relying on CTEs with RETURNING, window-function edge cases, or Postgres collations should be tested rather than assumed.
Cutting over a live production database is where this stops being a weekend project. If you would rather not learn these lessons on your own users, this is precisely the kind of work our custom software development team handles, and our nearshore development model keeps engineers in US Central hours so the migration window lands inside a normal business day instead of at 3 a.m.
Turso vs Traditional Databases
| Feature | Turso | PostgreSQL | PlanetScale |
|---|---|---|---|
| Edge replication | Yes, 35+ locations | No | Limited |
| Embedded replicas | Yes | No | No |
| Serverless | Yes | Requires setup | Yes |
| SQL dialect | SQLite | PostgreSQL | MySQL |
| Concurrent writers | Serialized | High | High |
| Branching | Yes | No | Yes |
| Cold starts | None | Possible | None |
When to Choose Turso
Choose Turso when:
- Global users need low latency
- Building edge-first applications
- Using serverless architecture
- Need simple, SQLite-compatible database
- Want embedded replicas for offline support
Consider alternatives when:
- Need advanced PostgreSQL features (JSONB, full-text search)
- Require complex transactions
- Already invested in MySQL/PostgreSQL ecosystem
Pricing
Plan limits move more often than the API does—treat the figures below as the shape of the pricing model and confirm the current numbers on Turso’s own pricing page before you build a budget around them.
Free Tier (Starter)
- 9GB total storage
- 500 databases
- 3 locations per database
- 1 billion row reads/month
- 25 million row writes/month
Scaler ($29/month)
- 24GB storage (then $0.75/GB)
- Unlimited databases
- 6 locations per database
- Embedded replicas
- Point-in-time recovery
Enterprise
- Custom storage
- Unlimited locations
- Dedicated support
- SLA guarantees
Common Patterns
Soft Deletes
export const users = sqliteTable("users", {
id: integer("id").primaryKey(),
email: text("email").notNull(),
deletedAt: text("deleted_at"),
});
// "Delete" a user
await db
.update(users)
.set({ deletedAt: new Date().toISOString() })
.where(eq(users.id, 1));
// Query only active users
const activeUsers = await db
.select()
.from(users)
.where(isNull(users.deletedAt));
Audit Logging
export const auditLog = sqliteTable("audit_log", {
id: integer("id").primaryKey(),
tableName: text("table_name").notNull(),
recordId: integer("record_id").notNull(),
action: text("action", { enum: ["create", "update", "delete"] }).notNull(),
oldData: text("old_data"),
newData: text("new_data"),
userId: integer("user_id"),
createdAt: text("created_at").default("CURRENT_TIMESTAMP"),
});
// Log changes
async function logChange(table: string, id: number, action: string, oldData?: any, newData?: any) {
await db.insert(auditLog).values({
tableName: table,
recordId: id,
action,
oldData: oldData ? JSON.stringify(oldData) : null,
newData: newData ? JSON.stringify(newData) : null,
});
}
Full-Text Search
SQLite has built-in FTS5:
-- Create FTS table
CREATE VIRTUAL TABLE posts_fts USING fts5(title, content, content=posts, content_rowid=id);
-- Populate FTS index
INSERT INTO posts_fts(posts_fts) VALUES('rebuild');
-- Search
SELECT * FROM posts_fts WHERE posts_fts MATCH 'javascript tutorial';
Why We Use Turso at Codebrand
At Codebrand, we’ve adopted Turso as our primary database for new projects. Here’s why:
Performance: Our clients’ users are global. Turso’s edge replication means fast database queries regardless of location.
Simplicity: SQLite is battle-tested and simple. No complex configuration, no connection pooling headaches.
Cost-Effective: The free tier handles most small-to-medium projects. Pay only when you scale.
Developer Experience: Combined with Drizzle ORM, we get type-safe queries with excellent autocomplete and error checking.
Serverless-Ready: Perfect for our Astro + Netlify stack. No cold start penalties, no connection limits.
How We Can Help
Whether you’re migrating to Turso or building on it from scratch, the work usually falls into four buckets:
- Architecture: schema design, primary placement, and deciding which reads may be stale
- Migration: moving off PostgreSQL or MySQL without a painful cutover
- Integration: wiring Turso into Astro, Next.js, or whatever framework you already run
- Optimization: index tuning, query shape, sync cadence, and replication strategy
Need help building this?
If the edge-versus-primary tradeoffs above sound like decisions you’d rather make with someone who has already made them, that’s what we do. Codebrand has been building production software from San Pedro Sula since 2020—including the CRM we run our own business on—and we work US Central hours as a nearshore development team, so you get real overlap instead of overnight ticket ping-pong. Tell us what you’re building and we’ll tell you honestly whether Turso is the right database for it: get in touch.
Keep reading
Related Articles

Web Development
The Fastest Web Tech Stack in 2026: Build Lightning-Fast Websites
Discover the fastest web development stack for 2026. Learn how to combine Astro, Turso, Tailwind, and edge hosting for sub-second page loads and 100 Lighthouse scores.

Web Development
Netlify Guide 2026: Zero to Production Fast
Master Netlify hosting in 2026. Learn deployment, serverless functions, forms, edge functions, and advanced features. Complete tutorial with real-world examples.

Web Development
Top 5 Web Development Technologies for 2026
Discover the top 5 web technologies dominating 2026. From Astro to Turso, learn what's worth your time and what's just hype. Based on real production experience from 300+ projects.
Do you want to read more articles?
Visit our blog to explore more content on web development, design, and digital marketing.