Plain CRUD with Drizzle
Not every entity should be event-sourced. For "rows in a table" workloads (users, products, posts, settings), Nwire's data adapter pattern is glue, not abstraction — you write real Drizzle, the framework owns the connection's lifecycle.
What you get
- Same Drizzle you'd use anywhere. End-to-end TypeScript inference. No
Repository<T>wrapper, no proxy layer. - Lifecycle for free. Pool closes during
runApp's shutdown sequence, after in-flight HTTP requests drain. - Readiness check.
/readyonly returns 200 whenSELECT 1succeeds. - DI registration.
ctx.resolve("db")works for tests + multi-tenancy.
Setup
1. Install
pnpm add @nwire/drizzle drizzle-orm pg
pnpm add -D drizzle-kit @types/pg2. Declare your schema (Drizzle, not Nwire)
// src/db/schema.ts
import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
email: text("email").notNull().unique(),
name: text("name").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});3. Build the client (one file, in your app)
// src/db/index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema.js";
export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });4. Wire the provider
// src/app.ts
import { createApp } from "@nwire/app";
import { drizzleProvider } from "@nwire/drizzle";
import { sql } from "drizzle-orm";
import { db, pool } from "./db/index.js";
export const app = createApp({
appName: "my-app",
plugins: [
drizzleProvider({
db,
close: () => pool.end(),
check: (d) => d.execute(sql`SELECT 1`),
}),
],
});That's it. The framework now:
- Boots the provider during
app.start()→dbis registered on the container - Runs
checkas part of/readyaggregation - Calls
pool.end()during graceful shutdown, after in-flight requests drain
Using it in a handler
Import the db from your own module — Drizzle's TypeScript inference flows through:
// src/users/actions.ts
import { defineAction } from "@nwire/forge";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { db } from "../db/index.js";
import { users } from "../db/schema.js";
import { UserWasRegistered } from "./events.js";
export const registerUser = defineAction({
name: "users.register",
description: "Marta creates an account.",
input: z.object({
email: z.email(),
name: z.string().min(1).max(80),
}),
emits: [UserWasRegistered],
handler: async ({ input }) => {
const [user] = await db.insert(users).values({
email: input.email,
name: input.name,
}).returning();
// Still emit the event — workflows + projections + audit hook on it.
// Drizzle owns the storage; events own the narrative.
return UserWasRegistered({ userId: String(user.id), email: user.email });
},
});When CRUD, when event-sourced?
Both paths run side-by-side. Pick per aggregate:
| Use Drizzle (CRUD) when… | Use Actor (event-sourced) when… |
|---|---|
| The entity is "rows in a table" with no rich lifecycle | The entity has named states (pending → approved → published) |
| You need ad-hoc joins / reports | You care about who did what when historically |
| Read patterns dominate (lookups, listings, exports) | Multiple workflows fan out from the same fact |
| Schema changes are flat — add a column | Schema changes need projections to backfill differently |
A single app can — and usually does — mix both. Your User is probably CRUD. Your Submission going through grading might be an actor.
Tests — pglite for zero-Docker DB
import { PGlite } from "@electric-sql/pglite";
import { drizzle } from "drizzle-orm/pglite";
import { drizzleProvider } from "@nwire/drizzle";
const client = new PGlite();
const db = drizzle({ client });
const app = createApp({
appName: "users",
handlers: [...userHandlers],
plugins: [...forgePlugins()],
})
.with(drizzleProvider({ db, close: () => client.close() }));
await app.start();
// ... run dispatches against a real in-process Postgres ...
await app.stop();No Docker. No flake. The same Drizzle API.
Migrations
The framework deliberately doesn't own migrations — Drizzle Kit already does that well. Run them from your deploy script:
pnpm drizzle-kit migrateOr wire it into a definePlugin({ boot }) that runs migrations on startup (good for dev, risky for prod — most teams gate migrations behind a separate job).