Skip to content
Strata

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#

  1. Create a database and token:

    turso db create my-site
    turso db show my-site --url
    turso db tokens create my-site
  2. Set DATABASE_URL=libsql://<db>-<org>.turso.io and DATABASE_AUTH_TOKEN=<token> in your platform’s environment variables. The names Turso’s Vercel integration sets, TURSO_DATABASE_URL and TURSO_AUTH_TOKEN, work too (getDatabaseConfig() in src/lib/env.ts reads both).

  3. Add the same two values as GitHub Actions secrets (repository Settings → Secrets and variables → Actions → Secrets tab), either as DATABASE_URL and DATABASE_AUTH_TOKEN or under the Turso names TURSO_DATABASE_URL and TURSO_AUTH_TOKEN, then run the Migrate database workflow from the Actions tab. It runs pnpm db:migrate against that database and runs again automatically whenever a merged change adds a file under drizzle/. 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.

Cloudflare and the Better Auth CLI

src/db/client.ts and src/lib/auth.ts read process.env through src/lib/env.ts and use relative imports so that the Better Auth CLI can load them outside Astro. On Cloudflare, process.env is populated thanks to the nodejs_compat flag in wrangler.jsonc. The workerd runtime cannot open file: databases, so pnpm dev:cloudflare expects a local libSQL server (turso dev).

Adding a table#

  1. Declare it in src/db/schema/app.ts.
  2. Run pnpm db:generate and review the SQL in drizzle/.
  3. Run pnpm db:migrate locally, commit the migration and deploy.