Best Boilerplates with Turso (SQLite Edge Database) in 2026
TL;DR
Turso is SQLite distributed globally with per-database pricing — perfect for multi-tenant SaaS where each customer gets their own database. Unlike Postgres (one shared DB with RLS), Turso lets you spin up a new SQLite database per user or per organization, with sub-millisecond reads from 300+ edge locations. In 2026, the best boilerplates for Turso are T3 Stack + Drizzle (most flexible), Hono.js starters (edge-native), and any boilerplate using Drizzle ORM (Turso has first-class Drizzle support). Here's how to set it up.
Key Takeaways
- Turso: SQLite at the edge — 300+ global replicas, scales to zero, $0 free tier (500 databases)
- libSQL: Turso's SQLite fork with HTTP API and replication support
- Multi-tenant killer feature: create one database per customer ($0.75/month/db on paid plan)
- Drizzle + Turso: the fastest path — Drizzle has native Turso/libSQL support
- Best for: read-heavy apps, globally distributed users, multi-tenant isolation
- Not best for: write-heavy workloads, complex analytical queries, teams wanting Postgres
What Makes Turso Different
Traditional Postgres (Supabase/Neon):
→ Single database
→ Multi-tenant via RLS (all users share tables)
→ Connection pooling required for serverless
→ ~50ms+ for edge/CDN requests to central DB
Turso:
→ SQLite files distributed globally
→ 300+ edge locations — reads from nearest replica
→ Per-database model: 1 database = 1 customer
→ HTTP API — no connection pooling needed
→ Sub-millisecond reads at edge
Use case: multi-tenant SaaS
Supabase: organizations share tables, RLS enforces isolation
Turso: each organization = separate SQLite database
→ perfect isolation, no RLS needed, easier GDPR deletion
Turso pricing (2026):
Free: 500 databases, 9GB storage, 1B row reads/month
Starter: $29/month — 10,000 databases, 24GB storage
Scaler: $119/month — unlimited databases, 480GB storage
At 500 organizations on the free tier: $0. The cost scales with your success, not upfront.
Setup: Drizzle + Turso in Any Boilerplate
npm install drizzle-orm @libsql/client
npm install -D drizzle-kit
// db/client.ts — Turso connection:
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
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 });
// db/schema.ts — SQLite-compatible Drizzle schema:
import {
sqliteTable, text, integer, real
} from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';
export const users = sqliteTable('users', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
email: text('email').unique().notNull(),
name: text('name'),
plan: text('plan', { enum: ['free', 'pro', 'enterprise'] })
.default('free').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' })
.$defaultFn(() => new Date()).notNull(),
});
export const subscriptions = sqliteTable('subscriptions', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
userId: text('user_id').references(() => users.id).notNull(),
stripeCustomerId: text('stripe_customer_id').unique(),
status: text('status').notNull(),
plan: text('plan').notNull(),
currentPeriodEnd: integer('current_period_end', { mode: 'timestamp' }),
});
export const posts = sqliteTable('posts', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
content: text('content'),
published: integer('published', { mode: 'boolean' }).default(false),
createdAt: integer('created_at', { mode: 'timestamp' })
.$defaultFn(() => new Date()),
});
// Usage — identical to Postgres Drizzle:
import { db } from '@/db/client';
import { users, posts } from '@/db/schema';
import { eq, desc } from 'drizzle-orm';
// Query:
const user = await db.query.users.findFirst({
where: eq(users.email, 'user@example.com'),
});
// Insert:
await db.insert(users).values({
email: 'new@example.com',
name: 'New User',
});
// Update:
await db.update(users)
.set({ plan: 'pro' })
.where(eq(users.id, userId));
Boilerplate 1: T3 Stack + Turso
Price: Free Stack: Next.js 15 + TypeScript + Drizzle + Turso + tRPC + Tailwind
The T3 Stack defaults to Prisma + Postgres, but switching to Drizzle + Turso is straightforward:
npm create t3-app@latest my-saas -- --noGit
# Select: TypeScript, Next.js, Tailwind, App Router
# DON'T select Prisma (we'll use Drizzle)
# Install Drizzle + Turso:
npm install drizzle-orm @libsql/client
npm install -D drizzle-kit
// drizzle.config.ts:
import type { Config } from 'drizzle-kit';
export default {
schema: './src/db/schema.ts',
out: './migrations',
driver: 'turso',
dbCredentials: {
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
},
} satisfies Config;
Turso-specific Drizzle commands:
# Generate migration:
npx drizzle-kit generate:sqlite
# Push schema directly (for development):
npx drizzle-kit push:sqlite
# Inspect existing Turso DB:
npx drizzle-kit introspect:sqlite
Boilerplate 2: Hono.js + Turso (Edge-Native)
Price: Free Stack: Hono.js + Drizzle + Turso + Cloudflare Workers
For truly edge-native SaaS (runs at Cloudflare's 300+ locations):
// src/index.ts — Hono on Cloudflare Workers:
import { Hono } from 'hono';
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client/http'; // HTTP client for edge
import * as schema from './schema';
import { eq } from 'drizzle-orm';
type Env = {
TURSO_DATABASE_URL: string;
TURSO_AUTH_TOKEN: string;
};
const app = new Hono<{ Bindings: Env }>();
// Create DB client per request (edge pattern):
function getDB(env: Env) {
const client = createClient({
url: env.TURSO_DATABASE_URL,
authToken: env.TURSO_AUTH_TOKEN,
});
return drizzle(client, { schema });
}
app.get('/api/user/:id', async (c) => {
const db = getDB(c.env);
const user = await db.query.users.findFirst({
where: eq(schema.users.id, c.req.param('id')),
});
return c.json(user);
});
app.post('/api/posts', async (c) => {
const db = getDB(c.env);
const body = await c.req.json();
const [post] = await db.insert(schema.posts).values(body).returning();
return c.json(post, 201);
});
export default app;
# wrangler.toml — Cloudflare Workers config:
name = "my-saas-api"
main = "src/index.ts"
compatibility_date = "2026-01-01"
[vars]
TURSO_DATABASE_URL = "libsql://your-db.turso.io"
# Secrets (not in wrangler.toml):
# wrangler secret put TURSO_AUTH_TOKEN
Performance: ~2ms API responses when data is read from nearest Turso replica. No cold starts (Cloudflare Workers are always warm).
The Multi-Tenant Killer Feature: Database Per Customer
This is Turso's most compelling use case for SaaS:
// Create a new database for each organization:
// lib/turso.ts
export async function createOrganizationDatabase(orgId: string) {
// Turso Management API:
const response = await fetch('https://api.turso.tech/v1/organizations/your-org/databases', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TURSO_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: `org-${orgId}`, // Each org gets a named database
group: 'default', // Replication group
}),
});
const { database } = await response.json();
// Store the database URL in your central metadata DB:
await centralDb.organization.update({
where: { id: orgId },
data: {
tursoDbName: database.Name,
tursoDbUrl: `libsql://${database.Name}.turso.io`,
},
});
// Create schema in the new org database:
const orgDb = getOrgDatabase(database.Name);
await orgDb.run(sql`CREATE TABLE IF NOT EXISTS posts (...)`);
}
// Get per-org database:
export function getOrgDatabase(dbName: string) {
const client = createClient({
url: `libsql://${dbName}.turso.io`,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
return drizzle(client, { schema: orgSchema });
}
// Route requests to correct org database:
// app/api/posts/route.ts
export async function GET() {
const session = await auth();
const org = await getOrganization(session.user.orgId);
// Each org queries their own database:
const db = getOrgDatabase(org.tursoDbName);
const posts = await db.query.posts.findMany();
return Response.json(posts);
}
Benefits of database-per-tenant:
- True data isolation (no risk of data leaks between orgs)
- GDPR deletion: delete the entire database → instant, complete
- Performance isolation (one customer's queries don't slow others)
- Independent backups per customer (useful for enterprise contracts)
- Customer can get a data export as a SQLite file
When Turso Is Right (and Wrong)
Turso wins:
- Global users needing low-latency reads (CDN-speed database reads)
- Multi-tenant SaaS wanting database-level isolation
- Edge/serverless apps (Cloudflare Workers, Vercel Edge)
- Read-heavy applications (Turso reads are extremely fast)
- Budget-conscious stage (500 databases free forever)
Turso loses:
- Write-heavy applications (SQLite has write locking — use Postgres for high-write scenarios)
- Complex analytics queries (use ClickHouse or BigQuery for OLAP)
- Real-time subscriptions (Turso has no equivalent to Supabase Realtime)
- Teams who want a hosted dashboard to browse data (use Supabase for developer UX)
- JSONB heavy usage (SQLite JSON support is limited vs Postgres)
Quick Comparison: Turso vs Alternatives
| Turso | Neon | PlanetScale | Supabase | |
|---|---|---|---|---|
| Type | SQLite (distributed) | Postgres | MySQL | Postgres |
| Free tier | 500 DBs, 9GB | 10GB | ❌ ($39/mo) | 500MB × 2 |
| Edge reads | ✅ Sub-ms | Limited | Limited | Limited |
| DB per tenant | ✅ Cheap | Expensive | Expensive | N/A |
| Realtime | ❌ | ❌ | ❌ | ✅ |
| Auth built-in | ❌ | ❌ | ❌ | ✅ |
| Drizzle support | ✅ Native | ✅ Native | ✅ | ✅ |
| Cold starts | None | Scales to 0 | None | Pauses |
Compare boilerplates by database support at StarterPick.