norav.dev
This site, and the CMS I wrote to run it. Next.js and raw PostgreSQL on one side, Vite and React 19 on the other, joined by a snapshot that's baked into every build and revalidated after the page loads.
- Next.js
- PostgreSQL
- Zod
- PGlite
- Vite
- React 19
- Tailwind 4
- Vercel
Context
norav.dev is my portfolio, and the CMS behind it is part of the portfolio. The site is Vite and React 19, prerendered at build time. The CMS is Next.js on raw PostgreSQL, with no ORM. The two share one package of block schemas. I wanted the site to show how I work, not just list it, and a CMS I can open in an interview does that. I built both, and I run both, alone.
The problem
A portfolio has to be fast and must never break. A CMS adds a database, an admin and a deploy, and each of them can fail. The site has to paint without waiting for the CMS. And a publish must not be able to ship a page that renders broken.

Decisions
- 01
Bake the content into the build, then revalidate
Each build bakes the published snapshot into the bundle, and every route is prerendered to HTML. The first paint makes no request to the CMS, and a CMS outage can't break a deploy. Once the page is idle, it asks the CMS for a checksum and swaps in newer content only if it differs. The cost: two copies of the content, and a publish reaches the first paint only after a rebuild.
- 02
One block registry for the admin, publish and the site
Each of the 28 block types is a Zod schema in a shared package. The admin draws its forms from it, publish validates every block against it, and the site takes its types from it. A page that would render broken can't be published. The cost: validation checks shape, not truth, so a false claim passes. And removing a block type takes two releases: retire its rows, then its code.
- 03
Raw SQL and append-only migrations, tested on an embedded Postgres
There are 35 numbered SQL migrations, never edited once shipped. The schema tests apply them all to PGlite, Postgres compiled to WebAssembly, and local development runs on it with no database server. Each publication is an immutable row, so any version can be read back. The cost: no ORM writes a migration for me, and content changes are SQL written by hand.
Outcome
On the mobile preset, the home page scores 100 in all four Lighthouse categories, with LCP at 1.3 seconds. Owning it has a cost, and it's real. In early November 2026, production was 14 migrations behind main, and the September rebuild's copy had never gone live. A 4.5 MB request limit on Vercel had quietly broken larger uploads. Now every release runs one rehearsed sequence: backup, rehearsal on a copy, dry run, then the change.
- SQL migrations, append-only
- 35
- API routes in the CMS
- 28
- automated tests: site, CMS and schema
- 594
- LCP on mobile, home page
- 1.3s
What I'd do differently
Was it worth it? As a content tool, no: a hosted CMS would have cost far less time for a site that changes once a month. As evidence of how I work, yes: I can open any layer of it in an interview, from a migration to the first paint. I'd wire the site rebuild into the release script on day one, and publish early. The September rebuild merged 21 PRs, and none of its copy went live for six weeks.