SQLite

Switch the project from PostgreSQL to SQLite.

For the complete documentation index, see llms.txt. Prefer markdown by appending .md to documentation URLs or sending Accept: text/markdown.

TurboStarter ships with PostgreSQL by default, but you can absolutely run it on SQLite if that fits your product better.

SQLite is a good option when you want a simpler local setup, a single file database for development, or a libSQL provider such as Turso in production. The important thing to understand is that SQLite is a customization rather than a one-line toggle.

PostgreSQL is still the default

At the time of writing, the starter is wired to PostgreSQL in packages/db/src/server.ts, packages/db/drizzle.config.ts, packages/auth/src/server.ts, and the schema files in packages/db/src/schema.

When SQLite makes sense

SQLite is a strong fit when you want:

  • local development without a database container
  • a simpler deployment story for smaller products
  • an edge-friendly database provider such as Turso/libSQL

You should usually stay on PostgreSQL if you need:

  • heavier concurrent write traffic
  • PostgreSQL-specific features
  • a fully drop-in experience with the starter's default schema and migrations

What you need to change

Moving to SQLite usually means updating these areas:

  • packages/db/package.json - add a SQLite driver
  • packages/db/src/env.ts - validate the new connection settings
  • packages/db/src/server.ts - initialize Drizzle with a SQLite client
  • packages/db/drizzle.config.ts - switch Drizzle Kit from postgresql to sqlite
  • packages/auth/src/server.ts - change Better Auth from provider: "pg" to provider: "sqlite"
  • packages/db/src/schema/* - replace PostgreSQL-only schema utilities with SQLite-compatible ones
  • packages/db/src/utils/index.ts - review helper types that still import PostgreSQL table types

The good news is that not everything needs to be rewritten. For example, helper utilities such as buildConflictUpdateColumns already support both PgTable and SQLiteTable.

Install the SQLite driver

For this setup, the most practical choice is libSQL, because the same driver works with both local SQLite files and remote Turso databases.

pnpm --filter @workspace/db add @libsql/client
pnpm --filter @workspace/db remove postgres

Update environment variables

For local development, point DATABASE_URL to a SQLite file. Drizzle ORM's libSQL tooling expects the file: prefix.

.env.local
DATABASE_URL="file:./.data/local.db"

If you use Turso, set the remote URL and auth token instead:

.env.local
DATABASE_URL="libsql://your-database.turso.io"
DATABASE_AUTH_TOKEN="your-token"

Ignore local database files

If you keep a local SQLite file inside the repository, add its directory to .gitignore so you don't commit the database by accident. A common choice is .data/.

Replace the database client

The current database package uses postgres-js. Swap it to the libSQL client in packages/db/src/server.ts.

packages/db/src/server.ts
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";

import { env } from "./env";
import * as schema from "./schema";

const client = createClient({
  url: env.DATABASE_URL,
  authToken: env.DATABASE_AUTH_TOKEN,
});

export const db = drizzle(client, {
  schema,
  casing: "snake_case",
});

You should also relax the env validation in packages/db/src/env.ts so it accepts both local file: URLs and remote libsql:// URLs:

packages/db/src/env.ts
export const preset = {
  id: "db",
  server: {
    DATABASE_URL: z.string().min(1),
    DATABASE_AUTH_TOKEN: z.string().optional(),
  },
} as const;

Switch Drizzle Kit to SQLite

Update packages/db/drizzle.config.ts to use the Drizzle Kit SQLite dialect:

packages/db/drizzle.config.ts
import { defineConfig } from "drizzle-kit";

import { env } from "./src/env";

export default defineConfig({
  out: "./migrations",
  schema: "./src/schema/index.ts",
  dialect: "sqlite",
  casing: "snake_case",
  dbCredentials: {
    url: env.DATABASE_URL,
    authToken: env.DATABASE_AUTH_TOKEN,
  },
});

Update Better Auth

The auth package is also PostgreSQL-first today. In packages/auth/src/server.ts, change the Better Auth Drizzle adapter provider:

packages/auth/src/server.ts
database: drizzleAdapter(db, {
  provider: "sqlite",
  schema,
}),

This is the key Better Auth change when you keep using the Drizzle adapter.

Convert the schema to SQLite

This is the largest part of the migration.

Today the starter's schema uses drizzle-orm/pg-core utilities such as pgTable and pgEnum in packages/db/src/schema/auth.ts and packages/db/src/schema/billing.ts. SQLite uses drizzle-orm/sqlite-core instead, so you need to review both files carefully.

The most common replacements are:

  • pgTable -> sqliteTable
  • pgEnum(...) -> text(..., { enum: [...] })
  • PostgreSQL-typed helpers such as PgTable / PgTableWithColumns -> SQLite-compatible or dialect-neutral equivalents
  • PostgreSQL-specific raw SQL such as excluded.column_name -> keep only where the target dialect supports it

For example, enum-like fields from packages/db/src/schema/billing.ts can be modeled as text columns:

packages/db/src/schema/billing.ts
import { sqliteTable, text } from "drizzle-orm/sqlite-core";

const subscriptionStatuses = [
  "active",
  "canceled",
  "incomplete",
  "incomplete_expired",
  "past_due",
  "paused",
  "trialing",
  "unpaid",
] as const;

export const subscription = sqliteTable("subscription", {
  id: text("id").primaryKey(),
  status: text("status", { enum: subscriptionStatuses }).notNull(),
});

The buildConflictUpdateColumns helper is already partly prepared for SQLite because it accepts SQLiteTable, but getOrderByFromSort still imports PostgreSQL-only types:

packages/db/src/utils/index.ts
import type { SQLiteTable } from "drizzle-orm/sqlite-core";
// replace PgTable / PgTableWithColumns imports as needed

Review PostgreSQL-only columns carefully

The largest schema changes are the pgEnum definitions in packages/db/src/schema/billing.ts and the PostgreSQL-only helper types in packages/db/src/utils/index.ts. This is not a blind search-and-replace migration.

Regenerate migrations from scratch

Once the schema and config are updated, generate a fresh SQLite migration set.

Because the existing migration history was generated for PostgreSQL, don't reuse it as-is for SQLite.

rm -rf packages/db/migrations
pnpm with-env turbo db:generate
pnpm with-env pnpm --filter @workspace/db db:migrate

After that, you can keep using the same Drizzle workflows:

  • pnpm with-env turbo db:generate
  • pnpm with-env pnpm --filter @workspace/db db:migrate
  • pnpm with-env pnpm --filter @workspace/db db:studio

If you're converting an existing project, this is the safest order:

  1. Commit your PostgreSQL version first.
  2. Switch the client, Drizzle config, and Better Auth provider.
  3. Convert the schema files from pg-core to sqlite-core.
  4. Update packages/db/src/utils/index.ts for SQLite-compatible typing where needed.
  5. Delete old PostgreSQL migrations.
  6. Generate fresh SQLite migrations.
  7. Smoke test sign-in, sign-up, billing, and organization flows.

Final notes

SQLite works well with the starter, but it is not currently the "default path". Treat it as an intentional database adapter swap, not just a connection string change.

If you want the smoothest setup, prefer @libsql/client, keep the schema conservative, and regenerate your migrations cleanly once the dialect switch is complete.

How is this guide?

Last updated on

On this page

Ship your startup everywhere. In minutes.Try TurboStarter