Drizzle (Postgres / SQLite / MySQL)
Not every entity should be event-sourced. For plain "rows in a table" workloads (users, products, posts, settings), @nwire/drizzle is glue, not abstraction — you write real Drizzle, and the framework owns the connection's lifecycle.
What you get
- Real Drizzle. End-to-end TypeScript inference. No
Repository<T>wrapper, no proxy layer. - Lifecycle for free. The pool closes during graceful shutdown, after in-flight HTTP requests drain.
- Readiness check.
/readyreturns 200 only whenSELECT 1succeeds. - DI binding.
ctx.resolve<DB>("db")works in handlers, tests, and multi-tenant scopes.
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 { forgePlugins } from "@nwire/forge";
import { drizzlePlugin } from "@nwire/drizzle";
import { sql } from "drizzle-orm";
import { db, pool } from "./db/index.js";
import { registerUser } from "./users/register-user.js";
export function buildApp() {
return createApp({
appName: "my-app",
plugins: [
drizzlePlugin({
db,
close: () => pool.end(),
check: (d) => d.execute(sql`SELECT 1`),
}),
...forgePlugins({ /* workflows, projections, actors */ }),
],
handlers: [registerUser],
});
}That's it. The framework now:
- Boots the plugin during
app.start()→dbis registered on the container - Runs
checkas part of/readyaggregation when probes are enabled - 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 { createApp } from "@nwire/app";
import { drizzlePlugin } from "@nwire/drizzle";
const client = new PGlite();
const db = drizzle({ client });
const app = createApp({
appName: "tests",
plugins: [drizzlePlugin({ 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).