Colophon

How this site is built, and why these particular choices. If you're a fellow Rails developer or just curious what powers a personal site that doesn't reach for a Next.js app, here it is.

Stack

  • Ruby on Rails 8.1 on Ruby 3.3.9. Server-rendered, full-stack, deliberately boring.
  • PostgreSQL 16 for everything — application data, Active Storage metadata, Solid Cache, rate-limit counters.
  • Hotwire (Turbo + Stimulus) instead of a JavaScript framework. The most complex Stimulus controller is the macro calculator on the tools page.
  • Tailwind CSS 4 via tailwindcss-rails. No PostCSS pipeline, no Node build step — Tailwind is invoked by a Ruby gem that bundles a static binary.
  • Import maps instead of bundling. ESM modules served as-is.
  • Puma behind Thruster for HTTP-level caching and X-Sendfile.
  • Heroku single-dyno deployment, Postgres Essential-0.

Conventions

  • Server-rendered, no SPA. Every page is a real URL; every form is a real form. View source and you'll see HTML.
  • No build step in Heroku. The Ruby buildpack precompiles assets; nothing else runs at deploy time besides db:prepare.
  • File-backed essays. The /writing section is plain Markdown files in config/essays/*.md with YAML front matter. No CMS, no admin, no spam vector.
  • Server-rendered SVG charts. The weight chart on /health is generated by a Rails helper that emits inline SVG. No client JS, no chart library.
  • Auth is the Rails 8 generator default, with an admin flag added so logged-in non-admins can't reach /admin.
  • Background jobs run in-process via the :async ActiveJob adapter — fine for one dyno. Discord webhooks for the contact + newsletter forms enqueue here.
  • Caching is Solid Cache backed by the primary Postgres. Rate-limit counters survive across Puma workers.

Security defaults

  • HTTPS forced; HSTS on; secure cookies.
  • Content Security Policy with per-request nonces. The two inline scripts (theme bootstrap, Google Analytics) carry valid nonces; everything else is rejected.
  • rack-attack throttling on top of per-controller rate_limit.
  • Honeypot fields on the contact + newsletter forms.
  • Brakeman + bundler-audit run on every CI build and block on warnings.

What I didn't reach for, and why

  • No React / Next / Vue. The site doesn't need a client-side state machine. Hotwire covers the few interactive pieces and keeps the page weight tiny.
  • No Redis. Postgres handles cache + queue at this scale. Less infra to operate.
  • No CMS. Markdown files in a Git repo are a CMS, just one with diffs and code review.
  • No JavaScript bundler. Import maps + a couple of Stimulus controllers ship roughly 30 KB of JS, gzipped.

Source

The repository is private, but the architectural choices are the interesting part — see this page and the essay on why I rebuilt it.