Building this site, and the arguments I had with myself
I could have shipped this site in a weekend on a template. I did not, and the reason is that I wanted the architecture of my own site to be something I could defend, out loud, to a skeptical engineer. So here is the defense.
The whole thing is one repo, one language, one platform. Next.js on the App Router, TypeScript everywhere, Postgres behind it, deployed on Vercel. There is no separate backend service, no second runtime, no Kubernetes. That sounds like laziness until you count the failure modes I deleted by refusing to add them.
One repo, and the discipline of saying no
The first argument I had with myself was whether the content worker deserved to be its own service. It fetches sources, calls an LLM, writes drafts. That is a real job, and every instinct I picked up in production says jobs like that belong somewhere isolated with their own deploy cadence.
Then I asked the question that actually matters: what does splitting it buy me here? Independent scaling I do not need. Independent deploys for a system with exactly one deployer. A second set of secrets to rotate, a second thing to monitor, a network hop between the worker and the database it writes to. The cost was concrete and the benefit was hypothetical, so the worker is a cron route in the same app. If it ever earns its own service, it can leave. Architecture should be a response to pressure, not a prediction of it.
CSS Modules over Tailwind, on purpose
This is the choice people will argue with, so let me be precise about it. Tailwind is fast, and the speed is real. But this site is mostly a creative object. It has a graffiti typeface in the header, three scripts rotating through an identity card, a drop cap on every article. The styling is not incidental to the site, it is the point of the site.
Tailwind moves styling into the markup, which is a fantastic trade when the styling is a means to an end. It is a bad trade when the styling is the end. I wanted to open a stylesheet and read a design decision, not decode a string of utility classes. So the cost I accepted is writing a flexbox row by hand more often than I would like. The thing I bought is that six months from now, the CSS still tells me why.
“A default is not a decision. Every time I picked the obvious tool, I made myself say out loud what it was costing me.”
The seam that matters most
Article bodies are not raw HTML. They are an ordered list of typed blocks: a lead, a paragraph, a heading, a quote. The article page decides what each block looks like. Nothing anywhere in the system hands a string of markup to the browser and hopes.
That was not a typography decision, it was a security and sanity decision, and it turned out to be the most valuable line I drew. It means an LLM writing drafts cannot inject markup into my site, because markup is not a thing it is allowed to produce. It can only produce blocks, and blocks are data. The renderer owns the presentation forever. When a machine is going to be writing into your system, the shape of what it is allowed to say is the actual safety mechanism, not the prompt you wrapped around it.
Mock data as an architectural tool
Every page on this site was built against a mock data layer: plain functions returning plain objects, shaped exactly like the database rows that will eventually replace them. Not because I was avoiding the database, but because I wanted to find out whether my components cared where their data came from. They do not. Swapping the mock for a real query means changing the body of a function and touching zero components.
That is a boundary you either have or you do not, and the cheapest time to find out is before the database exists. It also meant that when my Supabase project went to sleep and its host stopped resolving, my entire site kept building. The dependency I thought was load bearing turned out to be swappable, which is exactly what I had been hoping to prove.
The constraint I designed around
Vercel gives a hobby function sixty seconds. The draft worker has to fetch its sources, call a model, and write to Postgres inside that budget. I keep meeting engineers who treat a limit like that as an obstacle to route around. I think it is the most useful thing in the whole design.
Sixty seconds tells me a feature that cannot finish in a minute does not belong in that worker. It kills entire categories of scope creep before I can talk myself into them. The best constraints do not stop you from building the thing. They stop you from building the wrong thing, and they do it early, when changing your mind is still free.


