Database
Drizzle ORM with libSQL, the local file database, Turso in production and the migration workflow.
The template uses Drizzle ORM with the libSQL client. libSQL is SQLite-compatible, works from a local file with zero services, and is served over HTTP by Turso in production, which makes it usable from Node, Vercel, Netlify and Cloudflare Workers alike.
Files#
| File | Purpose |
|---|---|
src/db/client.ts |
Creates the libSQL client and the Drizzle instance |
src/db/schema/auth.ts |
Generated by Better Auth; do not edit by hand |
src/db/schema/app.ts |
Your own tables (contact_message, throttle, audit_log) |
src/db/schema/index.ts |
Re-exports every table for Drizzle and Better Auth |
drizzle.config.ts |
Drizzle Kit configuration (dialect: 'turso') |
drizzle/ |
Generated SQL migrations, committed to Git |
.github/workflows/migrate.yml |
Applies the migrations to the production database from GitHub Actions |
Local development#
DATABASE_URL=file:./.data/local.db (the default) stores the database in the ignored .data
folder. Create it and apply migrations with:
pnpm db:migrate
pnpm db:seed creates demo@example.com and admin@example.com (password password123),
and pnpm db:reset deletes the local file and re-applies every migration. The
scripts reference lists the rest.
Production with Turso#
-
Create a database and token:
turso db create my-site turso db show my-site --url turso db tokens create my-site -
Set
DATABASE_URL=libsql://<db>-<org>.turso.ioandDATABASE_AUTH_TOKEN=<token>in your platform’s environment variables. The names Turso’s Vercel integration sets,TURSO_DATABASE_URLandTURSO_AUTH_TOKEN, work too (getDatabaseConfig()insrc/lib/env.tsreads both). -
Add the same two values as GitHub Actions secrets (repository Settings → Secrets and variables → Actions → Secrets tab), either as
DATABASE_URLandDATABASE_AUTH_TOKENor under the Turso namesTURSO_DATABASE_URLandTURSO_AUTH_TOKEN, then run the Migrate database workflow from the Actions tab. It runspnpm db:migrateagainst that database and runs again automatically whenever a merged change adds a file underdrizzle/. Its first log lines list which of the four secret names the job can see. Variables, Dependabot secrets and the platform’s own environment variables are not visible to the workflow. From your machine the equivalent is:DATABASE_URL=libsql://… DATABASE_AUTH_TOKEN=… pnpm db:migrate
Any other libSQL-compatible host works the same way. To switch to PostgreSQL, replace
@libsql/client with a serverless-friendly driver, change the Drizzle dialect and regenerate
the Better Auth schema with --dialect postgresql.
Querying#
import { desc } from 'drizzle-orm';
import { db } from '@/db/client';
import { contactMessages } from '@/db/schema';
const recent = await db
.select()
.from(contactMessages)
.orderBy(desc(contactMessages.createdAt))
.limit(10);
The client is created once per module instance. On serverless platforms that is once per cold start, which is the recommended pattern for HTTP-based drivers.
Adding a table#
- Declare it in
src/db/schema/app.ts. - Run
pnpm db:generateand review the SQL indrizzle/. - Run
pnpm db:migratelocally, commit the migration and deploy.