Zero-proxy full stack · SQL + HTML
PostgreSQL is your web server.
An HTTP listener runs inside a Postgres background worker. Your app
is .sql handlers and .html templates —
no Node, no app tier, no ORM. The database serves the site.
Browser
│
▼
Caddy (TLS)
│
▼
PostgreSQL
├─ HTTP workers (extension)
├─ SQL handlers (your app)
└─ your data (right there)
A route is a folder. This is the whole page.
SQL returns JSON; HTML renders it. Nothing in between.
-- pages/posts/[id].sql
CREATE FUNCTION pgweb.pages__posts__id(req json)
RETURNS json AS $$
SELECT json_build_object(
'post', (SELECT row_to_json(p)
FROM posts p
WHERE id = (req->'path_params'->>'id')::int)
);
$$ LANGUAGE sql;
<!-- pages/posts/[id].html -->
<h1>{{ post.title }}</h1>
<article>{{ post.body }}</article>
What falls out of that decision
One container
Postgres, the HTTP server, and your app ship as a single image. Deploy is docker compose up.
Zero hops to data
Handlers run over SPI in the database's own process. No connection pool, no network round-trip per query.
Push in seconds
pg-web push upserts your files into framework tables. The live server picks them up on the next request.
A real dev loop
pg-web dev watches files, pushes on save, and live-reloads the browser.
No ORM layer
SQL is the interface. One transaction per request — commit on success, rollback on error.
An app you can pg_dump
Routes, templates, assets, and data live in one database. Backup and clone with tools you already trust.
From zero to served
cargo install pg-web
pg-web init my-app --template todo
cd my-app
pg-web up
pg-web migrate apply
pg-web push
Where this is honest. pg-web is Phase 1 software: the synchronous core is shipped and benchmarked, auth and background jobs are not here yet.
It is not an ORM, not a SPA framework, and it won't run on managed Postgres that blocks custom extensions (RDS et al.). The full story →