Skip to main content

Best Boilerplates with Turso (SQLite Edge Database) in 2026

·StarterPick Team
tursosqliteedge-databasesaas-boilerplatedrizzle2026

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

Compare boilerplates by database support at StarterPick.

Comments