Oolvay ships pre-integrated with Postgres, using Drizzle as its ORM. Every schema, every query, and every migration in the starter kit assumes a standard Postgres connection string. There is no hidden dependency on a specific vendor’s edge runtime or proprietary connection protocol.
In this guide, you’ll learn how Oolvay connects to Postgres, why Drizzle was chosen as the ORM and what it would take to swap in Prisma instead, the schemas that ship out of the box in auth-schema.ts, payments-schema.ts, notification-schema.ts, and blog-schema.ts, and when to reach for each of the six db:* scripts in your workflow.
Oolvay connects to the database using the postgres driver (postgres-js) through Drizzle’s drizzle-orm/postgres-js adapter. This is a plain TCP-based Postgres client, not a vendor-specific HTTP or edge connector.
import { env } from
0 / 2,000 characters
This choice is deliberate. Neon, for example, offers its own driver for this exact purpose, via @neondatabase/serverless and drizzle-orm/neon-http.
Using this pair would tie Oolvay's data layer to Neon's infrastructure specifically. Swapping to Supabase, RDS, or any other provider would mean rewriting db/drizzle.ts and its driver, not just changing DATABASE_URL. By using the standard postgres package against a normal connection string instead, Oolvay stays swappable across any provider that speaks the Postgres wire protocol, a connection string change, nothing more.
If you deploy to an edge runtime where TCP connections are unavailable, you will need a pooled or HTTP-based connection string from your provider rather than a direct one. This is a connection string change, not a code change.
Oolvay is tested thoroughly against Neon and Supabase. Both are first-class, documented integrations with their own setup guides.
Beyond these two, Oolvay should run with no changes or very minor changes on any standard Postgres provider, including
These all speak standard Postgres wire protocol, so a connection string swap in DATABASE_URL is typically all that’s required.
CockroachDB is Postgres-wire-compatible but is not plain Postgres under the hood. Certain SQL constructs, index behavior, and transaction semantics differ. Expect to need deeper schema and query tweaks rather than a drop-in swap if you choose CockroachDB.
Oolvay uses Drizzle ORM for every table definition, query, and migration in the codebase.
Both Drizzle and Prisma are mature, production-ready ORMs for TypeScript. Oolvay picked Drizzle for the following reasons, specific to what a starter kit needs to optimize for.
| Concern | Drizzle | Prisma |
|---|---|---|
| Query style | SQL-like query builder, close to raw SQL | Its own query API, abstracted further from SQL |
| Schema definition | Plain TypeScript files, no separate DSL | .prisma schema file, a separate language Prisma parses |
| Generated client | None needed at runtime, types come from your schema file directly | Requires a generate step (prisma generate) producing a client before types exist |
| Cold start / bundle size | Lightweight, no separate query engine binary | Ships a query engine binary, heavier for serverless and edge |
| Migration tooling | drizzle-kit with generate, migrate, push, studio | prisma migrate with its own workflow and shadow database |
| Learning curve for SQL-literate developers | Low, since most APIs map directly to SQL concepts | Slightly higher, since the abstraction has its own conventions |
| Type safety | Full, derived directly from pgTable definitions | Full, derived from the generated client |
Drizzle’s SQL-first design means the schema file you read in db/schemas/ is the same mental model you’d use writing raw SQL, with no separate schema language and no generated client step standing between a schema change and usable types. For a starter kit meant to be read, forked, and extended by developers with varying levels of ORM experience, this lower abstraction overhead matters.
Prisma is the stronger choice when you want a more guided, schema-first developer experience, a built-in studio with richer relation visualization, or when your team is already standardized on it. Both are good tools. Oolvay’s choice reflects what fits a lean starter kit best, not a claim that Drizzle is universally superior.
Oolvay ships only with Drizzle. If you prefer Prisma, you can swap it in, but understand the scope before starting.
Every file under db/schemas/ needs to be rewritten as a Prisma
schema.prisma model. The pgTable definitions, enums, and relations all
have Prisma equivalents, but the syntax is entirely different.
db/drizzle.ts needs to be replaced with a Prisma client singleton, and
every import of db across the codebase (route handlers, server actions,
webhook handlers) needs to point at the new client.
Every query written with Drizzle’s query builder (db.select().from(...),
db.insert(...), the relational query API) needs to be rewritten using
Prisma’s client API (prisma.user.findMany(...), and so on).
drizzle.config.ts and the db:* scripts in package.json need to be
replaced with Prisma’s CLI commands and a prisma generate step added to
your build pipeline.
This is a meaningful rewrite, not a drop-in replacement. Most founders are better served sticking with Drizzle unless Prisma is a hard organizational requirement.
All schema files live in db/schemas/. Each file owns a domain area and is composed together in db/drizzle.ts.
| File | Tables | Purpose |
|---|---|---|
auth-schema.ts | user, session, account, verification, passkey, ssoProvider, auditLog | Better Auth’s core tables plus Oolvay’s extensions (tier, theme preference, notification preferences, payment provider linkage) |
blog-schema.ts | category, post | The built-in blog and content system |
payments-schema.ts | subscriptions, orders, webhookEvents | Subscription state, one-time orders, and webhook idempotency log |
notification-schema.ts | notificationEvent, userNotification | In-app notification events and per-user read state |
api-key-schema.ts | apikey | API key issuance and rate limiting metadata for the API key plugin |
licenses-schema.ts | licenses | License records tied to a user |
enums-schema.ts | — | Shared Postgres enums (for example notification_type) referenced by other schema files |
All schemas are imported and merged into a single schema object in db/drizzle.ts, which is what Drizzle uses for its relational query API and what drizzle-kit reads when generating migrations.
export const schema = {
...authSchema,
...blogSchema,
...apiKeySchema,
...paymentsSchema,
...notificationSchema,
...licensesSchema,
apikey: apiKeySchema.apiKey,
}drizzle.config.ts points its schema glob at ./db/schemas/*.ts, so
db:generate picks up any file in that directory automatically when diffing
for a new migration. This is separate from db/drizzle.ts, where the schema
object is built by manually spreading each schema file’s exports. Adding a new
file to db/schemas/ does not register it with the app, you also need to
import it in db/drizzle.ts and add it to the schema object before its
tables are usable through Drizzle’s relational query API.
package.json defines six database scripts. Each has a distinct purpose and a distinct moment in your workflow where it’s the right tool.
| Script | Command | When to use it |
|---|---|---|
db:generate | drizzle-kit generate | After you change anything in db/schemas/. Diffs your schema files against the last migration snapshot and writes a new SQL migration file into migrations/. Does not touch your database. |
db:migrate | drizzle-kit migrate | Applies pending migration files from migrations/ to whatever database DATABASE_URL points at. This is the command you run in CI/CD and in production. Safe, incremental, and tracked. |
db:push | drizzle-kit push | Pushes your current schema files directly to the database, skipping migration files entirely. Fast for local prototyping, but it does not leave a migration history. Never use this against a production database. |
db:studio | drizzle-kit studio | Opens Drizzle Studio, a local browser GUI for browsing and editing rows in your connected database. Useful for quickly inspecting data without writing SQL. |
db:drop | drizzle-kit drop | Deletes a generated migration file you no longer want, removing it from migrations/ and from drizzle-kit’s tracking. Use this if you generated a migration by mistake before it was ever applied. |
db:reset | tsx scripts/reset-db.ts | Drops every table in your local database. Destructive, and does not re-run migrations afterward, follow it with db:migrate to restore a usable schema. Local development only. |
db:seed | bun run scripts/seed-blog.ts | Populates the blog tables (category, post) with sample content. Scoped to the blog schema specifically, not a general-purpose seed for every table. |
Edit a file in db/schemas/, for example adding a column to user.
Run bun db:generate. This writes a new migration file describing the
change.
Inspect the generated SQL file in migrations/ before applying it,
especially for destructive changes like dropping a column.
Run bun db:migrate to apply the migration to your local database.
bun db:studio to confirm the change landed as expected.Commit both the schema file change and the new migration file in
migrations/. The migration file is what gets run in production, so it must
be committed.
During early local prototyping, before you’ve settled on a schema shape, db:push is faster since it skips writing migration files for every small iteration. Switch to the db:generate plus db:migrate flow once your schema stabilizes, and always use that flow for anything destined for a shared or production database.