diff --git a/examples/basic/package-lock.json b/examples/basic/package-lock.json index b079d152..d5e8b5a9 100644 --- a/examples/basic/package-lock.json +++ b/examples/basic/package-lock.json @@ -19,15 +19,15 @@ } }, "../..": { - "version": "0.2.0", + "version": "0.3.1", "dependencies": { - "camelcase": "^9.0.0", - "capnweb": "file:packages/capnweb" + "camelcase": "^9.0.0" }, "bin": { "tg": "dist/cli.mjs" }, "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.18.6", "@electric-sql/pglite": "^0.5.4", "@eslint/js": "^10.0.1", "@standard-schema/spec": "^1.1.0", @@ -41,6 +41,7 @@ "@typescript/native-preview": "7.0.0-dev.20260707.2", "acorn": "^8.17.0", "better-sqlite3": "^12.11.1", + "capnweb": "file:packages/capnweb", "eslint": "^10.7.0", "fast-check": "^4.9.0", "pg": "^8.22.0", @@ -56,13 +57,17 @@ "node": ">=22" }, "peerDependencies": { - "@electric-sql/pglite": "^0.4.4", + "@electric-sql/pglite": "^0.4.4 || ^0.5.0", + "better-sqlite3": "^12.11.1 || ^13.0.0", "pg": "^8.20.0" }, "peerDependenciesMeta": { "@electric-sql/pglite": { "optional": true }, + "better-sqlite3": { + "optional": true + }, "pg": { "optional": true } diff --git a/examples/chat/package-lock.json b/examples/chat/package-lock.json index 5a125ef5..5d081138 100644 --- a/examples/chat/package-lock.json +++ b/examples/chat/package-lock.json @@ -30,7 +30,7 @@ } }, "../..": { - "version": "0.2.0", + "version": "0.3.1", "dependencies": { "camelcase": "^9.0.0" }, @@ -68,8 +68,8 @@ "node": ">=22" }, "peerDependencies": { - "@electric-sql/pglite": "^0.4.4", - "better-sqlite3": "^12.11.1", + "@electric-sql/pglite": "^0.4.4 || ^0.5.0", + "better-sqlite3": "^12.11.1 || ^13.0.0", "pg": "^8.20.0" }, "peerDependenciesMeta": { @@ -84,26 +84,6 @@ } } }, - "../../packages/capnweb": { - "version": "0.6.1", - "extraneous": true, - "license": "MIT", - "devDependencies": { - "@changesets/changelog-github": "^0.5.2", - "@changesets/cli": "^2.29.8", - "@cloudflare/vitest-pool-workers": "^0.12.10", - "@cloudflare/workers-types": "^4.20260205.0", - "@types/ws": "^8.18.1", - "@vitest/browser": "^3.2.4", - "pkg-pr-new": "^0.0.60", - "playwright": "^1.56.1", - "tsup": "^8.5.1", - "tsx": "^4.21.0", - "typescript": "^5.9.3", - "vitest": "^3.2.4", - "ws": "^8.19.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", diff --git a/examples/sqlite/package-lock.json b/examples/sqlite/package-lock.json index 8c446354..7f371552 100644 --- a/examples/sqlite/package-lock.json +++ b/examples/sqlite/package-lock.json @@ -19,15 +19,15 @@ } }, "../..": { - "version": "0.2.0", + "version": "0.3.1", "dependencies": { - "camelcase": "^9.0.0", - "capnweb": "file:packages/capnweb" + "camelcase": "^9.0.0" }, "bin": { "tg": "dist/cli.mjs" }, "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.18.6", "@electric-sql/pglite": "^0.5.4", "@eslint/js": "^10.0.1", "@standard-schema/spec": "^1.1.0", @@ -41,6 +41,7 @@ "@typescript/native-preview": "7.0.0-dev.20260707.2", "acorn": "^8.17.0", "better-sqlite3": "^12.11.1", + "capnweb": "file:packages/capnweb", "eslint": "^10.7.0", "fast-check": "^4.9.0", "pg": "^8.22.0", @@ -56,13 +57,17 @@ "node": ">=22" }, "peerDependencies": { - "@electric-sql/pglite": "^0.4.4", + "@electric-sql/pglite": "^0.4.4 || ^0.5.0", + "better-sqlite3": "^12.11.1 || ^13.0.0", "pg": "^8.20.0" }, "peerDependenciesMeta": { "@electric-sql/pglite": { "optional": true }, + "better-sqlite3": { + "optional": true + }, "pg": { "optional": true } diff --git a/site/src/layouts/BaseLayout.astro b/site/src/layouts/BaseLayout.astro index f47a359b..5d785e94 100644 --- a/site/src/layouts/BaseLayout.astro +++ b/site/src/layouts/BaseLayout.astro @@ -7,8 +7,8 @@ interface Props { } const { - title = "Typegres — Postgres tables as classes. Methods are your API.", - description = "Wrap Postgres tables in TypeScript classes. Mark methods exposed; clients compose typed queries against them — no routes, no GraphQL, no auto-CRUD.", + title = "Typegres: Postgres tables as classes. Methods are your API.", + description = "Wrap Postgres tables in TypeScript classes. Mark methods exposed; clients compose typed queries against them. No routes, no GraphQL, no auto-CRUD.", } = Astro.props; --- diff --git a/site/src/pages/_HomePage.tsx b/site/src/pages/_HomePage.tsx index eb70623c..bdb468fe 100644 --- a/site/src/pages/_HomePage.tsx +++ b/site/src/pages/_HomePage.tsx @@ -25,7 +25,7 @@ export default function HomePage() { title: "1. Decouple Your Interface from Your Schema - With All of Postgres, Fully Typed", description: 'Wrap your tables in a stable, public interface. You can refactor your "private" tables and columns without ever breaking clients.', - leftCode: `class User extends Table("users") { + leftCode: `class User extends db.Table("users") { // ... // Your public interface stays stable as your schema evolves @@ -48,8 +48,8 @@ const latest = await User.from() { title: "2. Your Interface Defines Your Data Boundaries", description: - "Allowed operations are just methods on your interface, including relations and mutations. Everything fully composable and typed.", - leftCode: `class User extends Table("users") { + "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.", + leftCode: `class User extends db.Table("users") { // ... todos() { @@ -57,7 +57,7 @@ const latest = await User.from() } } -class Todo extends Table("todos") { +class Todo extends db.Table("todos") { // ... update(fields: { completed?: boolean; title?: string }) { @@ -89,9 +89,7 @@ await todo.update({ completed: true }).execute(db);`, } class Api { - @expose() db = db; - - // Server-validated entry point — clients compose against this: + // Server-validated entry point. Clients compose against this: @expose(z.string()) forToken(token: string) { return User.from() @@ -99,19 +97,25 @@ class Api { } } -export const client = new RpcClient(...);`, - rightCode: `// Client-composed query — crosses the wire to a constrained -// interpreter, where the server validates the @expose surface: -const stream = client.run((api) => - api.forToken(token) +// The whole server: hand out the root capability over a WebSocket. +export default { + fetch: (request) => newWorkersRpcResponse(request, toRpc(new Api())), +};`, + rightCode: `const api = newWebSocketRpcSession( + \`wss://\${location.host}/ws\`, +) as unknown as ShimStub; + +// 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: + // Any Postgres function (\`ilike\`, window funcs, ...): .where(({ users }) => users.name.ilike("%alice%")) - .live(api.conn) -); - -// Re-yields on every committed mutation that matches: -for await (const rows of stream) render(rows);`, + // Pushed on every committed mutation that matches: + .live() + .observe({ onNext: (rows) => setUsers(rows) }), +);`, leftLabel: "api.ts", rightLabel: "frontend.tsx", leftLanguage: "typescript", @@ -178,6 +182,20 @@ for await (const rows of stream) render(rows);`, A TypeScript API framework that lets clients compose any queries they need within boundaries you control.

+ +
+ + $ + npm i typegres + + + Full instructions + + +
@@ -275,11 +293,11 @@ for await (const rows of stream) render(rows);`,

- Ready to build the next generation of database APIs? + Try it

- Experience the power of composable, capability-first database queries with full type safety and - AI-native architecture. + 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.

@@ -297,7 +315,7 @@ for await (const rows of stream) render(rows);`, className="inline-flex items-center justify-center gap-2 px-6 py-3 bg-gray-900 hover:bg-gray-800 dark:bg-white dark:hover:bg-gray-100 text-white dark:text-gray-900 font-medium rounded-lg transition-colors" > - Star on GitHub + View on GitHub
@@ -525,6 +543,34 @@ for await (const rows of stream) render(rows);`, {/* Other FAQs */}
+
+

+ 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)? @@ -555,23 +601,23 @@ for await (const rows of stream) render(rows);`, Q: How does the RPC layer actually work?

- A highly constrained interpreter that evaluates untrusted code (the same shape as - {" "}codemode for AI agents), inspired by{" "} Cap'n Web - - . The client serializes a closure that composes over a set of classes/methods, and the - server evaluates it in a single RPC call. There's also a{" "} + {" "} + 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{" "} - PR + this PR {" "} - in flight to make Cap'n Web itself work as a transport for Typegres. + lands upstream.

@@ -583,6 +629,12 @@ for await (const rows of stream) render(rows);`, 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?

@@ -596,7 +648,8 @@ for await (const rows of stream) render(rows);`, Q: What's the project status?

- This is a research preview and not ready for production use. Try the{" "} + Pre-1.0. Published on npm and usable today, but not recommended for production workloads yet. + Try the{" "} ; // Iterate explicitly so we can hold the iterator handle for diff --git a/site/src/pages/_PlayPageInner.tsx b/site/src/pages/_PlayPageInner.tsx index b7683fe3..01fe9133 100644 --- a/site/src/pages/_PlayPageInner.tsx +++ b/site/src/pages/_PlayPageInner.tsx @@ -221,7 +221,7 @@ declare function output(value: unknown): void;