19Decisions

7 entries, newest first.

Jul 8, 2026

Content as typed code, not a CMS

Projects, essays, testimonials, and credentials live as typed TypeScript modules validated by Zod schemas at module load. There is no CMS and no database to operate, patch, or breach.

Alternatives considered
  • A headless CMS: editorial convenience, at the cost of an operated third-party service with its own attack surface and uptime dependency.
  • A database: nothing on this site actually needs one; the genuinely dynamic surfaces (contact form, rate limiter, AI assistant) don't touch content storage at all.
Trade-off accepted

Every content edit requires a code change and a deploy, not a CMS save button. Accepted because a malformed record then fails the build, not the visitor; there's nothing left to patch or breach in production.

Jul 8, 2026

Dropped the keywords meta tag

Removed the `keywords` meta tag from every page's metadata entirely, rather than populating it.

Alternatives considered
  • Populate it anyway, as most starter templates default to.
Trade-off accepted

None found: search engines have ignored the keywords meta tag for over a decade, so keeping it would only be a maintenance cost (one more field to keep honest) for zero SEO benefit. Keyword strategy lives in titles, descriptions, and headings instead.

Jul 3, 2026

A status badge that reads real state, never hardcodes it

The hero AI assistant's status badge reads the actual X-Assistant-Mode response header from its own API route and reflects the real serving mode (live Gemini 2.0 Flash vs. curated fallback), rather than ever hardcoding "LIVE."

Alternatives considered
  • Hardcode "LIVE" in the UI regardless of which path actually answered: simpler, but an interface that overstates its own capability costs more credibility than the feature earns.
Trade-off accepted

The badge stays in a neutral state until the first exchange actually proves which path answered, costing a moment of visual ambiguity in exchange for never displaying a claim the system can't back up.

Jul 3, 2026

No APM SDK: a dependency-free error webhook instead

Used Next.js's built-in onRequestError instrumentation hook to POST uncaught errors to a configurable webhook URL, rather than installing an APM SDK.

Alternatives considered
  • Sentry or a comparable APM SDK: full-featured (breadcrumbs, session replay, release tracking), but a heavy runtime wrap for a portfolio's actual error volume.
Trade-off accepted

No breadcrumbs, session replay, or release tracking. In exchange: zero new dependencies, and errors reach any JSON-accepting sink (Slack, Discord, a log drain) instead of a silently-failing email provider going unnoticed.

Jul 3, 2026

A server-verified HMAC challenge instead of a third-party CAPTCHA

Replaced a client-side-generated-and-verified arithmetic challenge (which meant /api/contact accepted direct POSTs from any script) with a stateless, HMAC-SHA256-signed challenge minted and verified server-side, with a TTL and a minimum-age "time trap."

Alternatives considered
  • A third-party CAPTCHA (reCAPTCHA/hCaptcha): adds a dependency, a third-party script, and a consent/privacy surface.
  • Keep the original client-side check: already known insecure; the trust boundary was in the wrong place.
Trade-off accepted

The design is stateless, so a valid token is replayable within its 15-minute TTL. Accepted because the rate limiter already caps submissions per IP, and every replay still required one human-solvable round trip to obtain.

Jun 12, 2026

A fresh CSP nonce per request, over static prerendering

A cryptographically random nonce is minted for every request and embedded in script-src as 'nonce-…' 'strict-dynamic', removing 'unsafe-inline' from scripts entirely. Because the nonce changes per request, every document renders dynamically rather than being statically prerendered.

Alternatives considered
  • Static prerendering with a single build-time nonce: defeats the purpose; the same nonce reused forever isn't a nonce, and any script that ever carried it stays trusted indefinitely.
  • 'unsafe-inline' script-src: the common default, but it reopens inline-script XSS as an entire class of vulnerability.
Trade-off accepted

Every HTML document costs a server render on each request instead of being served from CDN-cached static output. Accepted in exchange for eliminating inline-script XSS as a class, not just mitigating it.

Jun 12, 2026

Upstash Redis via REST, not a heavier queue or a self-managed store

Distributed rate limiting via a fixed-window counter in Upstash Redis's REST API (plain fetch, no SDK), with an in-process sliding-window fallback for dev/unconfigured deployments, and fail-open on backend errors or timeouts.

Alternatives considered
  • A dedicated queue or self-managed datastore for counters: heavier infrastructure than a portfolio's traffic volume justifies.
  • In-memory-only limiting everywhere: leaks across serverless instances, since the effective limit becomes (instances × max) rather than a real global cap.
Trade-off accepted

Chose fail-open over fail-closed on limiter outage: for a contact form and a chat assistant, availability outranks strict throttling during the rare window the limiter itself is down.

Engineering Log · Ramon Logan Jr.