Typegres exposes SQL semantics to clients through a typed API: clients get the full query language; the underlying schema stays an internal detail. The type system is generated from pg's catalogs; the query API is a thin layer over composable SQL fragments; mutations, transactions, and row materialization all compose through the same abstractions.
Core tenets:
- Expose SQL semantics directly to clients through a typed API — full query power for clients, schema decoupled from the interface they see.
- The API is an abstract data type on top of the database: logic, permissions, and state transitions live alongside the data.
- Clients query that data type by composing methods.
Example:
class User {
static forToken(token: string) {
const user_id = validate(token);
return User.from().where((u) => u.id.eq(user_id));
}
@expose
id = (Int8<1>).column({ generated: true });
@expose
todos() {
return Todo.from().where((t) => t.user_id.eq(this.id));
}
@expose
name = (Text<1>).column();
}
class Todo {
@expose([z.string(), z.string()])
update(name: string, content: string) {
return super.update(...);
}
}Columns expose full Postgres types, including nullability. Every
non-side-effecting Postgres function is a method on the corresponding type. A
capability-based RPC endpoint exposes a subset of JS with the top-level class
(User) as the entry point; @expose methods are chainable and discoverable
by clients. The result: data, permissions, and state transitions live in one
place, in one language.
Driveris the low-level connection layer (PgDriver,PgliteDriver). It exposesexecute(sql),runInSingleConnection(fn),close().Databaseis the typed query surface on top — what user code interacts with. It holds aDriverand a query/hydrate/transaction API.
A Database is either pool-backed (every execute routes through the
driver's pool) or transaction-bound (carries a single-connection
ExecuteFn). Both are instances of the same class. transaction(fn) hands
the callback a transaction-bound Database:
await db.transaction(async (tx) => {
await tx.execute(User.insert(...));
await User.from().execute(tx); // fluent form
});There is no AsyncLocalStorage threading ambient context — the tx is
passed explicitly. Nested calls flatten because Transaction.transaction(fn) = fn(this), so callees that accept a Database don't have to know whether
they're getting the pool or a txn.
Transactions use pg's default isolation. No stricter level is imposed by the framework.
The query API is object-capability shaped: clients can only reach what the BE author exposed, and the builder primitives enforce that at the type level.
QueryBuilder,InsertBuilder,UpdateBuilder,DeleteBuilderare immutable. Every method returns a new instance — no mutable state to smuggle references through.where,select,on, etc. are callbacks evaluated against a fresh scope minted bybind(). Aliases are ephemeral to compilation, never stored on classes, so client code can't fabricate references to tables or rows outside the scope it was handed..execute(db),.hydrate(db),.one(db),.maybeOne(db)are fluent terminators that accept anyDatabase(pool or tx);db.execute(...)/db.hydrate(...)are the non-fluent equivalents.
hydrate materializes rows as class instances — each column field is an
Any wrapping a CAST(param) of the value, so methods on the class
(relations, derived columns, mutations) compose into follow-up queries
without breaking the capability chain.
All Postgres types are represented as TS classes. Functions are methods on
those classes. Nullability is tracked in the N extends number type
parameter (0 = null, 1 = non-null, 0 | 1 = maybe null).
Full hierarchy: Any → Anycompatible → Anyelement → Anynonarray →
concrete types. Generic container types (Anyarray<T>, Anyrange<T>) wire
through .of().
Types under src/types/generated/ are generated from the pg catalog
(pg_type, pg_proc, pg_operator) via pglite introspection:
npm run codegen
The generated files are committed. npm run codegen:check regenerates into
a temp dir and diffs against the committed copies — CI runs this to catch
drift between the pg version and the checked-in output.
Table codegen is separate: npx tg generate introspects a user's schema and
writes typed Table files into their project (uses typegres.config.ts).
sql is the escape hatch — a tagged template returning an immutable Sql
builder. Supports sql.param, sql.raw, sql.ident, sql.join. Fragments
compose via template nesting. Compiles to pg ($1) or sqlite (?) style.
The nix develop shell is the source of truth for tool versions:
nodejs_22, postgresql_17, act for local GHA runs. It also exports
DATABASE_URL pointing at the socket provisioned by bin/startpg, so tests
and codegen connect without any per-developer setup.
- Traditional app builders: one BE layer, full pg power, minimal boilerplate. FE has the tools to compose queries directly.
- "Vibe-coders": entire backend can be a single file; FE is decoupled.
- Future shapes:
- Core system-of-record where clients integrate directly.
- Agents constructing UIs on demand, rooted in the typed API.