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/*.mdwith 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
adminflag added so logged-in non-admins can't reach /admin. - Background jobs run in-process via the
:asyncActiveJob 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.