Skip to content
EzraDB
Product

pgforge — the TypeScript data layer.

Author schemas, business-logic guards, database functions, and queries in typed TypeScript. pgforge compiles the rules into the database itself — verified in CI, not asserted in the app — and it speaks EzraDB natively.

Bun first-classNode 22.6+PostgreSQL 15–18EzraDB dialect2,070 tests
01

Define the schema in TypeScript

Drizzle-shaped column builders with full compile-time row inference — $inferSelect and $inferInsert come straight from the table definition. No codegen step, no drift between your types and your tables.

pgforge/schema
import { pgTable, uuid, text, integer, timestamp, toIR } from "pgforge/schema"
import { call, emitEntity } from "pgforge"
 
const orders = pgTable("orders", {
  id: uuid("id").primaryKey().default(call({ name: "gen_random_uuid" })),
  organizationId: uuid("organization_id").notNull(),
  orderNumber: text("order_number").notNull(),
  items: integer("items").notNull(),
  createdAt: timestamp("created_at", { withTimezone: true })
    .notNull()
    .default(call({ name: "now" })),
})
 
// row types are inferred at compile time — no codegen step
type OrderRow = typeof orders.$inferSelect // { id: string; items: number; … }
type NewOrder = typeof orders.$inferInsert // defaults become optional
02

Attach guards the database enforces

Business rules are typed expressions — closed by construction, so injection is a compile-time impossibility. pgforge compiles them to PL/pgSQL triggers with typed SQLSTATE errors: the rule holds for every service, script, and human, forever.

guards → PL/pgSQL
const E = defineErrors({
  itemsPositive: { code: "PF001", message: "order items must be greater than zero" },
})
 
emitEntity({
  source: toIR(orders),
  pk: "id",
  orgScope: "organization_id",   // writes forced to the active workspace
  // closed expressions, never raw strings — injection-safe by construction
  guards: [{ when: le(newCol(orders, "items"), lit(0)), error: E.itemsPositive }],
})
// → PL/pgSQL the database enforces, no matter which app writes
03

Author database functions, not stored-proc strings

The op() builder writes PL/pgSQL functions from typed TypeScript. Its contract is the point: every op() body provably halts, stays transaction-neutral, and surfaces errors as typed SQLSTATEs — the algorithmic tail lives in a separate, replay-verified escape hatch.

op() — functions in TypeScript
import { op } from "pgforge"
 
// a database function, authored in TypeScript
const postOrder = op({
  schema: "api",
  name: "post_order",
  args: [{ name: "order_id", type: "uuid" }],
  returns: "void",
  build: (b) => {
    // linear steps: assign · selectInto · insert/update/delete · perform
    // finite branching: guard · if_ — bounded iteration: forEach
    // every body provably halts, stays transaction-neutral, and raises
    // typed SQLSTATE errors; unbounded loops live in escapeHatch()
  },
})
04

Query with types — and RLS on by default

The runtime client binds any Postgres driver. withAuth() turns your session into transaction-scoped settings so row-level security does the filtering in the database — your queries stay typed end to end.

pgforge/query
import { pgforge, nodePostgresDriver } from "pgforge/query"
 
const db = pgforge(nodePostgresDriver(pool), { schema })
 
// RLS-scoped by construction: the session becomes transaction-local
// GUCs, and the database's own policies do the filtering
const rows = await db.withAuth(session, (tx) =>
  tx.select().from(orders).orderBy("createdAt").limit(20).execute(),
)
// rows: fully typed — no cast, no codegen

VALIDATED BEFORE ANY DATA IS TOUCHED

The verify, replay, and behavioral kits run first.

Emitted SQL applies to an ephemeral throwaway database, every function is statically checked, and behavior is replay-compared before a migration ever reaches real data.

  • 2,070 tests across 117 files stand behind the compiler
  • ~98% line and function coverage, enforced as a per-file CI floor
  • Every emitted function checked by plpgsql_check in an ephemeral database
  • Replay-equivalence proven both ways before a migration ships
  • Tested against PostgreSQL 15–18 — one CI leg per major

BUILT FOR EzraDB

pgforge ships a first-class EzraDB dialect with a capability matrix, and its integration lanes prove the whole surface on the wire — schemas, guards, RLS policies, and the compiled job queue all apply and run against a fresh EzraDB. Even BACKTESTs can be authored from TypeScript.

WHERE IT GOES NEXT

Deploying TypeScript through pgforge directly into the engine* — the same guards and functions, running where the data lives. One language from your app to the storage layer, completing the move-the-compute-not-the-data thesis.

*In development — not yet production-ready.