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.
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.
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
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.
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
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.
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()
},
})
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.
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.
THE REST OF THE ENGINE