---
title: "What I'd do differently"
slug: what-id-do-differently
section: tech
date: 2026-07-17T11:00:00.000Z
canonical: https://roland.leth.ro/blog/tech/what-id-do-differently
---

Seven posts in, the rebuild has been live long enough that the things I called "more than this site needs" have had a chance to either earn their keep or expose themselves as decoration. This is the closer: a short retrospective on the verdict, the patterns I'd reach for again, and the handful of decisions I'd revisit if I started over.

## The over-engineering verdict

The recurring thread across the series was that I built defences past the actual threat model — the dummy bcrypt hash, IP HMAC pseudonymization, magic-byte MIME sniffing, log-string sanitization, timing-safe bearer comparison — and flagged each as more than this site needs. Living with them for a while, I can split the list cleanly into two columns.

**Earned their keep as patterns, not as production defences.** Nothing here prevented an actual incident. The dummy hash hasn't blocked a timing probe; the IP HMAC hasn't stopped a rainbow-table attack; the magic-byte sniff hasn't caught a spoofed `image/png`. The site sees the traffic of a personal blog, and the realistic threat model on a personal blog is "occasional bot tries the login page and gives up." What these patterns did do is calibrate my judgment for the next time the threat model isn't hypothetical. I now have a feel for what "by the book" looks like across half a dozen surfaces, and that's the part I'd build again.

**The actual cost was negligible.** Two hundred lines of auth, twelve bytes of magic sniffing, one centralized `sanitizeLogString` call. None of it is load on the runtime, none of it makes the code harder to read, none of it has needed maintenance. The cost-of-keeping-it is essentially zero, which is what made it worth doing on a low-stakes project. On a fast-moving startup codebase, the same defences would be the wrong call — not because they're wrong, but because the maintenance budget would be allocated elsewhere.

## Patterns I'd reach for again

A short list of the things that, regardless of project size, I'd pull forward to day one:

- **The `parseJsonBody` helper.** Single chokepoint for every request body, paths-only logging, contract test that asserts submitted values never appear in logs. Cost: about 60 lines including the helper. Benefit: every future API route inherits the discipline for free.
- **The audit-log tag union.** A `const [...] as const` array of tags, a derived TypeScript type, one `auditLog(tag, payload)` call per successful admin write. Typos fail at compile time; aggregator grep sets stay valid; the cost of adding a new admin resource is one line. Cheap to set up early, costly to retrofit once the call sites multiply.
- **`useOptimisticMutation` with the `MutateResult` discriminant.** Two production consumers and a clean enough shape that the third one (when it shows up) will land in under ten lines. The `superseded` vs. `failure` split is the part that pays dividends — without the discriminant, the third consumer would have re-invented the same race I already paid for.
- **Cache without `now`, filter at read time.** The future-dated content pattern. Generalizes to anything where the cache key shouldn't include the "is this currently visible?" decision: scheduled releases, feature-flag rollouts keyed on time, anything with a `valid_until`.
- **`React.cache()` for per-request dedupe.** Cheap, scoped, and removes the duplicate work that the server-component model lets you accidentally introduce by splitting a page across `generateMetadata` and the page body. I'll be adding it reflexively from now on.

## Things I'd revisit on a do-over

**The posts schema.** The Posts table predates this rebuild by years and survived the migration mostly unchanged. There are columns I'd model differently if I were drawing it fresh today, but the table gets me 90% of what I want, and the cost of a schema migration plus a backfill across two hundred rows plus updating every read path is more than I'm willing to spend chasing the remaining 10%. It's the kind of thing I'd modernize if I were starting over, and won't touch as long as the current shape isn't actively chafing.

**The projects schema.** This one I'd actually revisit. Tags and buckets currently live in fields that weren't designed for them; every time I add a project I end up massaging the data to fit, and there are features I've talked myself out of building because the model would have to bend further first. A proper tags table and a real category type would unblock more than they'd cost. The reason it hasn't happened yet is the same reason as for posts: the existing data works well enough that the migration tax keeps losing the argument against "do it later." But unlike posts, "later" here means "when I want to build the feature that needs it," and that feature is on the list.

**The cache.** `unstable_cache` was, in retrospect, a learning exercise more than a load requirement. A site with two hundred posts and personal-blog traffic doesn't need cache padding for page-1 reads or read-time `datetime` filtering — direct DB queries on every request would have been fine, and would have sidestepped Next's continuing churn around the caching APIs. I'd still build the future-dated puzzle the same way (the design's correct), but I'd reach for caching only once there was a real bill or a real latency number that warranted it, not because the framework dangles the helper at me.

## Closing the series

Eight posts about a personal site is more than a personal site warrants, which is on-brand for the whole exercise. The point of the rebuild wasn't the site — it was the chance to build something end-to-end on the current generation of the tools I use professionally, take every decision seriously, and write down what I learned while it was still fresh. The site itself is the artifact; the seven previous posts are the lab notebook.

Thanks for reading along. There's [one postscript][next] still to come — the tooling that keeps me out of the admin — and after that the next thing I publish here probably won't be about Next.js.

If any of the series saved you a debugging hour, or if you've shipped something similar and want to compare notes, [@roland.leth.ro][] is the place.

[prev]: /blog/tech/three-markdown-pipelines-and-a-theme-toggle "Three markdown pipelines and a theme toggle"
[next]: /blog/tech/the-tools-that-keep-me-out-of-the-admin "The tools that keep me out of the admin"
[series]: /blog/tech/the-shape-of-a-next-js-app "The shape of a Next.js app"
[@roland.leth.ro]: https://bsky.app/profile/roland.leth.ro "@roland.leth.ro on Bluesky"
