Skip to main content

Database matrix

Everything goes through Drizzle ORM, so any Drizzle-supported driver works. Tested combinations:

DatabaseDriverRuntimeNotes
SQLite@libsql/client (drizzle-orm/libsql)NodeLocal file or Turso
SQLitebetter-sqlite3 (drizzle-orm/better-sqlite3)NodeSynchronous, fast local
SQLiteD1 (drizzle-orm/d1)WorkersNative Cloudflare binding
PostgreSQLpostgres (drizzle-orm/postgres-js)Node + WorkersUse Hyperdrive on Workers
PostgreSQL@neondatabase/serverless (drizzle-orm/neon-http)Node + WorkersHTTP driver, edge-friendly
PostgreSQL@electric-sql/pglite (drizzle-orm/pglite)NodeEmbedded Postgres, great for tests

Examples

// libsql (local file or Turso)
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
const db = drizzle(createClient({ url: "file:./data.db" }));

// better-sqlite3
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
const db = drizzle(new Database("data.db"));

// D1 (Workers)
import { drizzle } from "drizzle-orm/d1";
const db = drizzle(env.DB);

// postgres-js
import postgres from "postgres";
import { drizzle } from "drizzle-orm/postgres-js";
const db = drizzle(postgres(env.DATABASE_URL));

// Neon (HTTP, edge-friendly)
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
const db = drizzle(neon(env.DATABASE_URL));

// PGlite (embedded, tests)
import { PGlite } from "@electric-sql/pglite";
import { drizzle } from "drizzle-orm/pglite";
const db = drizzle(new PGlite());

Pass whichever db you build to useResource/createCovara — or wrap it with trackMutations first if you have custom routes.

Migrations

Covara doesn't impose a migration tool — use drizzle-kit:

npx drizzle-kit generate # generate migration SQL from your schema
npx drizzle-kit push # push schema directly (dev)

On D1, apply migrations via wrangler — see Workers deployment.

Internal tables (covara/db)

The auth subsystem persists state in four framework-owned tables. The covara/db subpath provides canonical schemas, an idempotent migrator, a generic seeder, and pool-sizing guidance.

TablePurposeUsed by
auth_sessionsServer-side sessionsDrizzleSessionStore
auth_accountsLinked OAuth/OIDC accountsAuth.js / federated adapters
auth_api_keysHashed API keysAPI-key credential flow
auth_verification_tokensEmail/magic-link/reset tokensVerification flows

Timestamps are stored as Unix-epoch integers on SQLite ({ mode: "timestamp" }) and as TIMESTAMP on Postgres.

Spread the dialect-appropriate tables into your Drizzle schema so drizzle-kit and queries see them:

import { internalSchema } from "covara/db";

export const schema = {
...internalSchema("sqlite"), // or "postgresql"
// ...your own tables
};

Individual tables are exported too (authSessions, authAccounts, authApiKeys, authVerificationTokens default to SQLite; *Sqlite / *Pg are explicit).

Framework migrations

migrateInternal(db, { dialect? }) runs CREATE TABLE IF NOT EXISTS (+ CREATE INDEX IF NOT EXISTS) for every internal table — idempotent and safe to call on every boot. autoMigrate(db) is the convenience wrapper; the dialect is inferred from the Drizzle db.

import { autoMigrate } from "covara/db";
await autoMigrate(db); // run once at startup

DDL runs through the passed Drizzle db (db.run on libsql/D1, db.execute on postgres/pglite), so it's Workers-safe — no Node fs at runtime.

Seeding

seed(db, { tables }) / createSeed() performs insert-or-ignore (ON CONFLICT DO NOTHING), so dev/staging data applies deterministically and repeatably. It works with any Drizzle table.

import { createSeed } from "covara/db";

await createSeed()
.table(usersTable, [{ id: "1", email: "demo@example.com" }])
.run(db);

Connection pooling

Use recommendedPoolConfig(driver) for sane defaults; tune to your origin's connection limit.

DrivermaxNotes
postgres-js10Long-lived Node process; keep max below (DB limit ÷ instances). Use a pooler URL for serverless.
neon1Serverless HTTP driver is connectionless per request; use the -pooler endpoint.
pglite1Embedded single-connection engine; pooling N/A.
libsql1HTTP/WS client multiplexes over one connection.
d11Request-scoped binding; avoid long transactions.
hyperdrive5Hyperdrive pools at the edge; keep the Worker-side max small.
import { recommendedPoolConfig } from "covara/db";
import postgres from "postgres";

const { max, idleTimeoutMs, connectTimeoutMs } = recommendedPoolConfig("postgres-js");
const client = postgres(url, { max, idle_timeout: idleTimeoutMs / 1000, connect_timeout: connectTimeoutMs / 1000 });

Search indexes

The built-in search adapters create their own backing tables — SQLite FTS5 or Postgres tsvector — so full-text search needs no extra service on either database.