Decisions
Server Components by default
Nearly every page is a read of content the visitor didn’t personalize, so pages render on the server. Client components go at the leaf, only where an interaction needs one: the hero that types its answers, the header’s controls, the inspector.
Tailwind v4, configured in CSS
Tokens and the utility namespace sit in one file, twelve lines apart, so they can’t drift. Dark mode is defined per token, not retrofitted. Focus rings are always visible, and animations do nothing under reduced motion. Every color, radius, and duration on this page resolves to a token.
A component library I own, not one I installed
ChosencksUI, in this repoBuilt on shadcn’s patterns, with Radix for behavior and the source copied in. Every value is rewritten onto these tokens.
RejectedAn installed UI libraryIt would ship someone else’s UI and run a second design system alongside this one.
Radix stays a dependency, and a justified one: focus management, keyboard interaction, and ARIA are where a subtle mistake is invisible until it reaches someone using a screen reader.
cksUI stands in the same relation to this site that VimUI does to the product I work on. The portfolio is an example of the claim rather than a description of it.
Neon over Supabase
ChosenNeon + DrizzleScales to zero, resumes instantly. Speaks Postgres over HTTP, so there’s no connection pool to exhaust.
RejectedSupabaseIts edge is Auth + row-level security — unused here, since auth is Auth.js. Complexity stays, payoff doesn’t.
The decider was practical: free-tier Supabase pauses after about a week idle, and the worst moment for that is a hiring manager opening a letter three days after I sent it. Cost: no table-editing UI, no file storage.
Drizzle over Prisma — lighter runtime, no codegen in the deploy path, migrations as plain SQL reviewable in a PR. One file knows the database is Neon.
OAuth, where a password would have done
For one user, a hashed password in an env var would be adequate security in eighty lines. I chose OAuth because OIDC is how modern auth works, and every enterprise SSO product is the same flow with a different issuer. I wanted to build it, not read about it.
ChosenAuth.js + database sessionsEvery leg of the code exchange leaves an inspectable row. Deleting a session row ends it immediately.
RejectedClerk, and JWT sessionsClerk’s value proposition is hiding the mechanics. A signed JWT stays valid until it expires.
Authentication isn’t authorization: GitHub will prove who any of its users is, but has no opinion on who may edit my portfolio. So a sign-in callback checks the login against an allowlist. No proxy-level guard: Next’s docs say that layer suits optimistic checks and real authorization belongs next to the data, so every admin route calls the guard itself.
Deleting six tables when the requirement changed
The schema was built for “tailored CVs” — positions, bullets, skills, tags, and case studies, joined per opening with overrides. It was a misreading that survived long enough to become six tables.
The actual need was cover letters. A CV is assembled from parts; a letter is prose addressed to one reader. Nothing to select, nothing to reorder.
ChosenDelete the six tablesCase studies are files and the experience page is structured data, so none of them had a reader left.
RejectedKeep them for laterUnused schema is a claim about the future that has to be maintained and explained.
The most useful thing I did to this database was take most of it out.
The URL is the credential
No login, because asking a hiring manager to register to read a cover letter is a good way to not have it read. The boundary is token entropy: 32 bytes from a CSPRNG, generated in the repository layer, never accepted as an argument.
The trade-off: anyone with the link can read and forward it. It’s a letter written to be handed to someone I have never met, so exposure is bounded by what it already is. It is also HTML only, because a downloadable copy would be a second artifact of the same content, stale the moment a sentence changes. Mitigations: revoke, rotate, expire, noindex, and an identical 404 for revoked, expired, draft, and nonexistent alike, so a recipient can’t learn a link was turned off — or that a company was ever sent one.
Screens from inside the product, behind a personal link
Most of the case studies are about work inside Vimocity’s app, and those screens aren’t this site’s to publish. So the files never enter the repository. They live in a private Blob store, and a hiring team sees them through a personal link: a signed token with an expiry, set as a cookie.
ChosenSigned links, checked per imageNo database and no accounts. The image route verifies the token before reading the store, and answers anything else with a 404.
RejectedA shared password, or publishing the screensA password can’t be revoked for one person. Publishing would put an employer’s product in a public repository.
Pages stay static: each gated figure renders both versions, and the theme script picks one before first paint. Without access, the frame keeps the screenshot’s size, with neutral shapes blurred past recognition and a request for access, so it reads as part of the case study rather than a gap.
View logging, minimal on purpose
“Did they open it” is useful, but the visitor is a counterparty in a hiring process who consented to nothing. No cookies, no fingerprinting, no third-party analytics. IP stored only as a salted hash; rotating the salt severs the link to everything logged — which is why the salt is secret, since hashing an IP isn’t strong anonymization. The write happens after the response is sent and swallows its errors.