Skip to main content

Best Boilerplates with Turso (Edge SQLite) 2026

·StarterPick Team
Share:

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

TursoNeonPlanetScaleSupabase
TypeSQLite (distributed)PostgresMySQLPostgres
Free tier500 DBs, 9GB10GB❌ ($39/mo)500MB × 2
Edge reads✅ Sub-msLimitedLimitedLimited
DB per tenant✅ CheapExpensiveExpensiveN/A
Realtime
Auth built-in
Drizzle support✅ Native✅ Native
Cold startsNoneScales to 0NonePauses

Turso vs Neon vs Supabase: The Practical Decision

Choosing between Turso, Neon, and Supabase comes down to three questions: Do you need real-time features? Do you need auth and storage bundled with your database? Are your reads globally distributed?

Supabase wins decisively when you want the most functionality for the least infrastructure decisions. Auth, storage, real-time subscriptions, and PostgreSQL in one dashboard — this combination reduces the number of services you need to configure and maintain. For a solo founder who wants to ship fast, Supabase's all-in-one model is the practical choice, even if individual components aren't the absolute best at their specific function.

Neon wins when you want standard PostgreSQL with excellent developer ergonomics — specifically the database branching feature and the generous free tier. Neon is purely a database service, so you'll pair it with a separate auth solution (Clerk, Auth.js, Better Auth) and a separate file storage solution (Cloudflare R2, UploadThing). The additional configuration is real overhead, but you get best-in-class tools at each layer. Neon's edge-compatible serverless driver also makes it viable for Vercel Edge Functions where Supabase's standard PostgreSQL connection isn't.

Turso wins when your specific use case is multi-tenant isolation, globally distributed reads, or you're building on Cloudflare Workers where sub-millisecond database reads matter. These are narrower requirements than Neon or Supabase cover, but when they're your requirements, Turso is the clearly correct choice. The SQLite data model also brings a philosophical simplicity — one database file per tenant means data isolation, backup, and deletion are straightforward operations.

Limitations of the SQLite Model at Scale

Turso's SQLite foundation creates constraints that don't exist with PostgreSQL-based databases. Understanding these constraints before committing to the architecture prevents hitting walls later.

SQLite's write performance is inherently single-threaded — only one write can occur at a time. Turso's architecture mitigates this by routing all writes to a primary database and replicating to read replicas, but if your application has high concurrent write volume (100+ writes per second to the same database), SQLite's locking model becomes a bottleneck. Applications with heavy reads and moderate writes — content platforms, analytics dashboards, documentation tools — fit the SQLite model well. Applications with high-concurrency writes — real-time collaboration, trading platforms, multiplayer games — need PostgreSQL's concurrent write handling.

The absence of advanced PostgreSQL features is a real constraint for some SaaS data models. Full-text search in SQLite is limited compared to PostgreSQL's GIN indexes and tsvector. JSON querying in SQLite lacks PostgreSQL's rich JSONB operators. Window functions and CTEs are available in modern SQLite but have less comprehensive support than PostgreSQL's implementation. If your data model relies heavily on these features, the migration from Turso to a PostgreSQL database is significantly more complex than migrating between PostgreSQL providers.

The tooling ecosystem around SQLite is smaller than PostgreSQL's. Fewer database visualization tools support SQLite natively. Database management tools, BI connectors, and ETL pipelines mostly target PostgreSQL and MySQL first. For data teams that need to query your application database with tools like Metabase, Tableau, or dbt, PostgreSQL provides a broader selection of compatible tools. Turso's HTTP API allows custom integrations, but you're more likely to need to build custom connectors.

Schema Management and Migrations with Turso

One underappreciated aspect of Turso's multi-database architecture is the schema management challenge. When each tenant has their own database, running a schema migration means applying it to potentially hundreds or thousands of individual databases. Turso's schema feature (available on paid plans) addresses this: you define a parent schema database, and child databases inherit the schema. When you push a migration to the parent, Turso propagates it to all child databases automatically.

Without schema groups, managing migrations across hundreds of tenant databases requires a custom script that iterates through all databases and applies migrations to each. This works but adds operational complexity — tracking which databases received which migrations, handling failures mid-propagation, and verifying consistency across all databases.

For the multi-tenant-per-database pattern to be maintainable at scale, design your schema management approach before you have more than a few dozen tenant databases. The Turso schema feature makes this sustainable at scale. The alternative — routing all tenants to a shared database with RLS — makes schema changes easier but loses the isolation benefits that make Turso's multi-database approach compelling in the first place.

Local development with Turso deserves specific mention: the turso dev command starts a local SQLite server that mimics Turso's API, allowing development without network latency or account credentials. Combined with Drizzle's drizzle-kit push:sqlite for local schema sync, the local development experience is excellent for individual developers. For team development, shared Turso development databases work similarly to how shared staging databases work with PostgreSQL providers. The free tier's 500 databases per organization means every developer on a team can have their own development database with room to spare for feature branch testing environments.

Integrating Turso into an Existing Boilerplate

Most SaaS boilerplates default to PostgreSQL via Supabase, Neon, or Prisma. Switching an existing boilerplate to Turso is a one-afternoon project if the boilerplate uses Drizzle ORM; it is a larger effort if it uses Prisma. The primary changes: swap the Drizzle adapter from drizzle-orm/postgres-js to drizzle-orm/libsql, update your schema to use SQLite column types instead of PostgreSQL column types, regenerate migrations, and update environment variables. The application query code changes nothing — Drizzle's query builder API is identical across adapters.

For boilerplates using Prisma, the migration is more significant. Prisma has a separate SQLite provider, but the SQLite data model in Prisma has limitations compared to PostgreSQL: no enums, no arrays, and limited JSON support. For boilerplates that use Prisma enums or arrays in their schema, the migration requires refactoring those schema fields to use text columns with application-level validation. This is manageable but requires testing every data path. If the boilerplate's Prisma schema is complex, it may be faster to keep Prisma and use Turso's HTTP API directly for the specific queries where you want edge-local reads, rather than migrating the entire data layer.

The environment variable setup for Turso in any boilerplate is two variables: TURSO_DATABASE_URL (the libsql:// URL for your Turso database) and TURSO_AUTH_TOKEN (the database auth token from the Turso dashboard). Add these to your .env.local for development and to your deployment platform's environment variable configuration for production. Turso provides separate tokens per database, which is useful for the multi-tenant pattern where each tenant's database has its own token.

Turso's Embedded Replica Feature

One of Turso's less-discussed features is embedded replicas: the ability to cache the entire SQLite database locally within your application process and serve reads from the local copy while syncing writes to Turso's cloud. This is not relevant for serverless or edge deployments (which don't have persistent local storage), but it is highly relevant for long-running Node.js processes like Railway or Fly.io-hosted backends.

With an embedded replica, database reads are true local reads — they access a SQLite file on the same machine, with no network round-trip at all. Latencies of under 1ms for reads are typical, faster than even the closest edge replica. Writes still go to Turso's primary database with network latency, but for read-heavy SaaS applications this trade-off is excellent. The sync happens automatically in the background; your application always reads the most recent committed data without manual cache management.

// Embedded replica setup for persistent Node.js processes
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';

const client = createClient({
  url: 'file:local.db',              // Local SQLite file for reads
  syncUrl: process.env.TURSO_DATABASE_URL!,  // Remote Turso for sync
  authToken: process.env.TURSO_AUTH_TOKEN!,
  syncInterval: 60,                  // Sync every 60 seconds
});

export const db = drizzle(client, { schema });

This configuration is particularly powerful for SaaS backend processes that perform heavy analytics or reporting queries. Instead of running complex aggregate queries against a remote database, the full dataset is available locally. A dashboard that aggregates data across all tenant records can run in sub-millisecond time rather than the hundreds of milliseconds a remote database query would require.

The embedded replica pattern is also useful for handling read spikes. When many users simultaneously trigger read-heavy operations (exporting reports, loading large dashboards), all reads are served from the local SQLite file without any database connection pressure. Write volume — creating records, updating subscription status — still goes through Turso's cloud database. For SaaS products with asymmetric read-to-write ratios (typical for B2B analytics or content tools), this pattern dramatically reduces infrastructure costs and eliminates read-throughput scaling concerns. The trade-off is that the local replica has a short sync lag for writes made by other processes, which is acceptable for most SaaS data (eventually consistent within the sync interval configured in the client).


Compare boilerplates by database support at StarterPick.

Compare Drizzle and Prisma for Turso and Neon: Drizzle vs Prisma for boilerplates 2026.

Explore Neon and PlanetScale as PostgreSQL alternatives: Best SaaS boilerplates with Neon and PlanetScale 2026.

See database options in the context of the full SaaS stack: Ideal tech stack for SaaS in 2026.

The SaaS Boilerplate Matrix (Free PDF)

20+ SaaS starters compared: pricing, tech stack, auth, payments, and what you actually ship with. Updated monthly. Used by 150+ founders.

Join 150+ SaaS founders. Unsubscribe in one click.