Skip to content

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. /ready only returns 200 when SELECT 1 succeeds.
  • DI registration. ctx.resolve("db") works for tests + multi-tenancy.

Setup

1. Install

bash
pnpm add @nwire/drizzle drizzle-orm pg
pnpm add -D drizzle-kit @types/pg

2. Declare your schema (Drizzle, not Nwire)

ts
// 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)

ts
// 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

ts
// 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()db is registered on the container
  • Runs check as part of /ready aggregation
  • 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:

ts
// 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 lifecycleThe entity has named states (pending → approved → published)
You need ad-hoc joins / reportsYou 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 columnSchema 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

ts
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:

bash
pnpm drizzle-kit migrate

Or 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).

MIT licensed.