vihaanvaghela.com
A personal archive built to outlast its own framework — CMS, essay pipeline, and an operator console with no URL.
- Next.js 16
- TypeScript
- Prisma
- PostgreSQL
- Tailwind
- Bun
- Vercel
This site. One Next.js application serving three systems that share a database and nothing else: the public archive, a CMS I wrote rather than installed, and an operator console that has no address at all.
I built it because a personal site assembled from someone else's template would have documented nothing about how I think, which is the only thing it is for.
The problem
The requirement that shaped everything: this has to still work in ten years, and it has to be able to show its own history.
That rules out most of the obvious answers. A hosted platform decides what your archive looks like and can withdraw the decision. A static site generator makes publishing a deploy, and anything that turns writing into a build step means less writing gets done. A database-backed CMS fixes that and introduces the opposite problem: the About page, which changes twice a decade, now lives behind a login instead of in version control where its diffs are readable.
The second requirement was awkward. I wanted a VECTOR operator console reachable from this site without being part of this site.
The approach
Content splits by how often it changes and who needs to read its history. Essays, projects, timeline entries and VECTOR documents are database rows, publishable with no deploy. The About page and site configuration are modules in the repository, because their diffs are the interesting part.
The console is reached by typing a trigger into the search bar. It is not linked, not in the sitemap, not in robots.txt, and every one of its paths returns a bare 404 to an unauthenticated request — byte-identical to a path that has never existed. That is a user-interface property and explicitly not the security model: every credential check happens server-side, and someone who extracts the trigger from the JavaScript bundle arrives at the same locked door as someone who guessed. Argon2id password, then an emailed one-time code, then a WebAuthn passkey.
Architecture
Next.js 16 App Router on Bun, Prisma over Postgres, Tailwind, deployed on Vercel. Twenty-one route handlers, all declaring the Node runtime explicitly, because two dependencies are compiled binaries — Argon2 and Prisma's query engine — and no edge isolate will load them.
The provenance split is the decision I am most attached to. Post and VectorDoc are
separate models holding what is superficially the same thing. A post is personal
writing and is human-written, which is why that model has no field to record anything
else — the absence is the rule. A VECTOR document is AI-assisted and then reviewed,
and the model records both facts. Publishing one that was never reviewed is refused in
code, because a pipeline step nothing enforces is a step that gets skipped.
Security headers live in next.config.ts rather than a proxy, so they travel with the
application. The session cookie is __Host- prefixed, HTTP-only and SameSite=strict.
Markdown renders without raw HTML, and dangerouslySetInnerHTML appears nowhere.
Results
Live, with every public route served from the database, an RSS feed, a sitemap that reflects what is actually published, and a full-text search that the console hides behind.
The deployment is the part worth recording, because it went badly and the record is the point. Cloudflare Pages could not run the application at all — three hard incompatibilities, not a configuration problem. Vercel could, but SQLite could not follow it there. Moving to Postgres broke the container path that had been working and left the documentation describing a database that no longer existed. Then Neon's pooled connection string turned out to be the wrong one to run migrations through.
None of that is in the design. All of it is in the git history.
Lessons learned
- Ship the thing that makes writing easy. Every decision that made publishing require a deploy was a decision to write less.
- Documentation rots faster than code, and more quietly. Changing the database provider silently invalidated six statements across two files and a Dockerfile, none of which failed a test. The code complained immediately. The docs told nobody.
- Obscurity is a UI property, never a control. Hiding the console behind a search trigger is good design and worth nothing as security. Writing the second half down is what stops the first half from quietly becoming the plan.
- Encode the rule you actually mean. "Please review AI-assisted docs before publishing" is a wish. A publish path that refuses is a rule.