build a typed postgres data layer with drizzle: schema, queries, migrations, and an RLS pattern. trigger on "set up the database", "data layer", "orm", "drizzle", or "typed queries".
---
description: build a typed postgres data layer with drizzle. schema, queries, migrations, RLS pattern. trigger on "set up the database", "data layer", "orm", "typed queries".
---
# typed data layer
the SaaS spine: a typed schema, safe queries, and migrations, without losing tenant isolation. uses the verified drizzle-orm module.
## intent
take a builder from "i need a database" to a typed schema with migrations and a tenant-isolation pattern that actually holds. the failure mode this prevents: an orm-only setup that filters by tenant in app code, which one forgotten where-clause turns into a cross-tenant data leak.
## inputs
- a postgres database + connection string
- DATABASE_URL in env
- drizzle-kit for migrations
## procedure
### step 1, define the schema with types
```js
const { pgTable, uuid, text, timestamp } = require('drizzle-orm/pg-core');
const orgs = pgTable('orgs', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
});
```
### step 2, query with inferred types
```js
const { drizzle } = require('drizzle-orm/node-postgres');
const db = drizzle(pool);
const rows = await db.select().from(orgs);
```
### step 3, generate + apply migrations
use drizzle-kit to diff the schema and emit SQL migrations, never hand-edit the db.
### step 4, enforce isolation in the database, not just the app
for multi-tenant apps add postgres RLS policies so the database refuses cross-tenant reads even if app code forgets a filter.
## decision points
- migrations: generated + reviewed, committed to the repo, never ad-hoc.
- RLS: mandatory for multi-tenant, the orm filter is a convenience not a guarantee.
- connection pooling: use a pooler for serverless or you exhaust connections.
## output contract
a typed schema, generated migrations in the repo, type-inferred queries, and (for multi-tenant) postgres RLS policies that enforce isolation at the database.
## outcome signal
success means a deliberately filter-less tenant query returns zero foreign rows because RLS blocks it. if isolation depends only on remembering a where-clause, the layer is unsafe.
don't have the plugin yet? install it then click "run inline in claude" again.
expanded inputs with env var details and postgres version, detailed each procedure step with explicit inputs and outputs, added edge cases for timeouts and empty results, clarified RLS setup and tenant context injection, added dev workflow decision point.
the SaaS spine: a typed schema, safe queries, and migrations, without losing tenant isolation. uses drizzle-orm to enforce type safety and database-level access control.
take a builder from "i need a database" to a typed schema with migrations and a tenant-isolation pattern that actually holds. the failure mode this prevents: an orm-only setup that filters by tenant in app code, which one forgotten where-clause turns into a cross-tenant data leak. use this skill when setting up a new postgres database for a multi-tenant app, migrating to typed queries, or hardening an existing data layer against isolation bugs.
postgres://user:password@host:port/dbname)install drizzle and set up the config file
npm install drizzle-orm drizzle-kit pgdefine the schema with typed columns
set up the drizzle client
export const db = drizzle(pool)generate and review the initial migration
drizzle-kit generate:pgapply migrations to postgres
drizzle-kit migrate or execute migration files manually via psqlwrite typed queries using the drizzle API
enable postgres RLS (for multi-tenant)
ALTER TABLE table_name ENABLE ROW LEVEL SECURITYset the tenant context before queries (for multi-tenant)
db.execute(sqlSET app.current_tenant_id = ${org_id}`)migrations: generated or hand-written? always generate migrations from schema.ts using drizzle-kit, commit the SQL to the repo, never hand-edit the database directly. if you need a custom migration (e.g., data transformation), create a separate .sql file in drizzle/migrations and mark it as manual.
RLS enforcement: mandatory or optional? for single-tenant apps, RLS is optional. for multi-tenant, RLS is mandatory, the app-layer tenant filter is a second line of defense not the first. if you skip RLS, a bug in app code becomes a data leak.
connection pooling: local dev or production? in dev you can use a direct postgres connection. in production or serverless, use a connection pooler (pgBouncer, neon, supabase) or you exhaust connections and queries hang or fail.
schema changes during development: how to iterate? use drizzle-kit drop to nuke migrations and the schema, then regenerate from your schema.ts. do this only in dev, never in production. for production, always generate a migration, review it, and apply it in order.
empty result sets: how to handle? drizzle returns an empty array, not null, so check rows.length === 0 or use optional chaining. if you expect a single row, use db.query.table.findFirst() with proper error handling for not-found cases.
network timeouts or auth failures: catch db query errors in your application layer, log them, and fail gracefully. if DATABASE_URL is missing or invalid, the drizzle client will error at runtime, not at schema definition time. test your connection on startup.
success means:
you know it worked when:
drizzle-kit migrate and get a production-identical database in secondsoriginal author: implexa