SQL-over-RPC, Safely
A TypeScript API framework that lets clients compose any queries they need within boundaries you control.
$npm i typegresFull instructions1. Decouple Your Interface from Your Schema - With All of Postgres, Fully Typed
Wrap your tables in a stable, public interface. You can refactor your "private" tables and columns without ever breaking clients.
class User extends db.Table("users") {// ...// Your public interface stays stable as your schema evolvescreatedAt(): Timestamptz<1> {return this.metadata["->>"]("createdAt").cast(Timestamptz);return this.created_at;}}
// Compiles to the single SQL query you'd write manually.const latest = await User.from().orderBy(({ users }) => [users.createdAt(), "desc"]).limit(1).execute(db);
2. Your Interface Defines Your Data Boundaries
Allowed operations are just methods on your interface, including relations and mutations. Everything fully composable and typed. This is encapsulation 101, applied to your tables.
class User extends db.Table("users") {// ...todos() {return Todo.from().where(({ todos }) => todos.user_id.eq(this.id));}}class Todo extends db.Table("todos") {// ...update(fields: { completed?: boolean; title?: string }) {return Todo.update().where(({ todos }) => todos.id.eq(this.id)).set(() => fields);}}
const user = ...// The only way to get a todo is through a user:const todo = await user.todos().where(({ todos }) => todos.id.eq(todoId)).one(db);// The only way to update a todo is via the hydrated instance:await todo.update({ completed: true }).execute(db);
3. Expose your API over RPC, Safely
Give clients a composable query builder with your inescapable data boundaries. Compose queries in the client with rich Postgres features and functions as primitives.
class User extends db.Table("users") {// ...}class Api {// Server-validated entry point. Clients compose against this:@expose(z.string())forToken(token: string) {return User.from().where(({ users }) => users.token.eq(token));}}// The whole server: hand out the root capability over a WebSocket.export default {fetch: (request) => newWorkersRpcResponse(request, toRpc(new Api())),};
const api = newWebSocketRpcSession(`wss://${location.host}/ws`,) as unknown as ShimStub<Api>;// Client-composed query. Crosses the wire over Cap'n Web,// where the server validates it against the @expose surface:const sub = await doRpc(api, (a) =>a.forToken(token).select(({ users }) => ({ id: users.id, name: users.name }))// Any Postgres function (`ilike`, window funcs, ...):.where(({ users }) => users.name.ilike("%alice%"))// Pushed on every committed mutation that matches:.live().observe({ onNext: (rows) => setUsers(rows) }),);
Try it
The playground runs Postgres in your browser via PGlite (no signup, no server) against a real demo schema. Typegres is pre-1.0 and not recommended for production workloads yet.
Frequently Asked Questions
API Frameworks: Typegres vs Hasura vs PostgREST
| Typegres | Hasura | PostgREST | |
|---|---|---|---|
| Schema coupling | Decoupled | Tightly coupled | Tightly coupled |
| Client composition | Full composable queries | GraphQL queries | REST endpoints |
| Authorization | Capability-based | RLS + permissions | RLS |
| Refactor safety | Safe schema evolution | Breaking changes | Breaking changes |
| Maturity/Ecosystem | Early/Experimental | Mature | Mature |
Typegres vs ORMs (Prisma, Drizzle, etc.)
Unlike ORMs (which are local dev tools), Typegres is designed for exposing your database over RPC. You can use it alongside your ORM, or as a standalone API layer.
| Typegres | ORMs (Prisma, Drizzle, etc.) | |
|---|---|---|
| Purpose | API framework | Local dev tool |
| Security model | Capability-based | N/A |
| Refactor safety | Safe schema evolution | Breaking changes |
| Maturity/Ecosystem | Early/Experimental | Mature |
Q: What dialects do you support?
Postgres and SQLite. Both surfaces are code-generated from the engine itself: Postgres from its catalog, SQLite from its documented functions. Each dialect gets its own real functions and operators rather than a lowest-common-denominator abstraction over both.
Drivers: node-postgres and PGlite (WASM Postgres, which the playground on this site runs on) for Postgres; better-sqlite3 and Cloudflare Durable Object storage for SQLite.
Q: What about tRPC?
tRPC removes most endpoint boilerplate, but it doesn't compose: the client can only call procedures that already exist.
So every new question is another procedure: join these two datasets, filter on something nobody anticipated, aggregate differently. Typegres pushes the composition itself to the server, so the client asks a new question without you shipping a new endpoint.
Q: What about raw SQL + Row Level Security (RLS)?
tl;dr Raw SQL + RLS has existed for almost a decade. Still, no one lets untrusted SQL run against their database.
A raw SQL approach has many drawbacks, but the most fundamental is applications need application code, not just SQL (e.g., calling external APIs, JSON validation).
Q: What about query performance?
Every query maps directly 1:1 to the single Postgres query you'd expect.
Note that relations are expressed as correlated subqueries, not raw joins, which modern versions of Postgres should optimize. This idea may be revisited later.
Q: How does the RPC layer actually work?
Cap'n Web is the transport, giving you capabilities, promise pipelining and live subscriptions over a single WebSocket. The client serializes a closure that composes over the classes and methods you exposed, and the server evaluates it against that surface in a single RPC call. Cap'n Web ships bundled with Typegres until this PR lands upstream.
Q: What's the actual security model under the hood?
The model is capability-based security. Instead of reactive security (a blacklist) the framework explicitly guides you to define your allowed surface area (your classes and methods) and enforces that all queries go through it.
The shape is close to GraphQL: the relations you define form a graph, and clients traverse it from a root capability, usually the row representing the current user. The difference is what they can do once they're inside those boundaries. GraphQL clients traverse edges; Typegres clients get the database itself: joins, aggregations, and every builtin function and operator.
Q: What about DoS?
Currently we recommend query timeouts. (Other options include cost calculations and limiting number of tables allowed per query).
Q: What's the project status?
Pre-1.0. Published on npm and usable today, but not recommended for production workloads yet. Try the playground to see the latest features and open a discussion or issue on GitHub if you have questions.