The Smallest Sensible Rails Stack, Behind Roadletter Lite
Stage 1 of Roadletter Lite is done. After two weeks there is an app with a sign-in page and a dashboard that says “Nothing here yet.” It took about three to four hours of my time, less than I expected and less than in my previous projects.
This is week two of eleven. There are no issues to write and no readers to send to after this stage. There is only the ground everything else will stand on.
Some things are cheap only at the start. Two languages, the structure of the stylesheets, the checks that run on every change: added later, each of them means touching every file twice. So the foundation got a week of its own.
In the first post I wrote that I did not know whether eleven weeks are too few or too many. One short stage does not answer that. The foundation is the most standard part of any Rails app, and the hard weeks are still ahead. But it is a first data point.
What is there after week two
One admin can sign in. There is no sign-up page, because Lite has exactly one account, created on the server.
The admin area speaks English and German. The language is stored with the admin and used everywhere, also on the sign-in page and in the password reset email. The browser’s language is never read.
Light and dark mode follow the system setting, and the layout works on a 390 px phone screen.
The dashboard is still empty. It says hello and points to the next step: connecting Amazon SES, the service that will send the email.
Every change to the code runs through automatic checks before it goes in: the tests, tests in a real browser, style checks, a security scanner, a check for dependencies with known vulnerabilities, and a check that both languages have every text.
A design read by a user, not a parser
The design existed before the code. Every component and every view was designed in pen.dev, with the help of Claude Code and Claude Design, then written down as design documentation and built in the app.
pen.dev misses some of the features that a heavyweight like Figma offers when you design for code. So Claude had to interpret where the file was silent, and its interpretations were on point. It got the responsive behaviour right.
I expected Claude Code to treat the design as a technical dependency, a set of measurements to copy. Instead it built it from the perspective of a user.
Small, but nothing missing
The stage plan calls this “the smallest sensible stack.” Small is easy: leave things out. Sensible means leaving out only what you will not have to add back later. A real app still needs sign-in, background jobs, email, two languages and tests. Everything else is one more thing to install, update, back up and understand.
Rails 8.1 on Ruby 4.0. Rails brings sign-in, background jobs, caching, email and live updates out of the box. The app Rails generated was committed unchanged as the first commit, so anyone reading the history can see exactly what Lite adds to the defaults.
No Redis. Many Rails apps run a second server, Redis, for background jobs and caching. Here jobs, cache and live updates all run on the database (as Solid Queue, Solid Cache and Solid Cable). The job runner lives inside the web server process, so production is one container.
No Node, no build step. There is no JavaScript toolchain to install or keep up to date. So far the app needs only two small pieces of JavaScript, Stimulus controllers that close a message and the account menu. The browser loads them as they are, through importmap, with nothing to compile first.
Plain CSS, no framework. One file holds the design tokens, the colours, sizes and fonts, with the dark values next to the light ones. Cascade layers fix the order of the rules, so nothing needs !important. Eleven stylesheets, about 800 lines.
Three self-hosted fonts. Literata, Instrument Sans and IBM Plex Mono, about 290 KB together, under the SIL Open Font License. None of them comes from a font CDN, because a font loaded from another host tells that host who opened the page.
15 gems at runtime, most of them Rails’ own defaults. The only addition so far is rails-i18n, for German dates and numbers.
One detail for developers: records use UUIDv7 instead of counting IDs. Rails signs the tokens in its email links but does not encrypt them, so a record’s ID can be read out of every confirmation and unsubscribe link. A counting ID would reveal a reader’s place on the list and let anyone estimate its size. A UUIDv7 reveals only when the record was created.
Why SQLite is enough for a newsletter tool
At the centre of the stack sits SQLite: a database that is a single file, with no database server next to the app.
The usual advice is to start with a real database server, because you might need it one day. For most apps that is good advice. But Lite has one admin, one newsletter and a few thousand readers at most. That is little data, and almost never two people writing at the same time.
SQLite allows one writer at a time. That is fine when every write is short and the sending happens in background jobs. App data, job queue, cache and live updates each get their own file, and production is one server, one container and one volume. Backups will be copies of those files, taken while the app runs.
The honest limit: this does not scale to several servers. Lite does not need to. The decision record in the repository lists what it makes easier and what it makes harder.
Where the AI needed a correction
In the first post I promised to show where I had to correct the AI. Here are four cases from this week, and who caught them.
The AI’s own tests. The password reset that Rails generates accepted an empty form. It saved an empty password, reported “Password has been reset” and signed out every session. A test caught it, and the reset now refuses an empty password.
A separate review. After the build, a fresh Claude session with no memory of the work checked the code against the stage plan. It found eight issues, none rated above low. One of them: the address of a password reset link, secret token included, landed in the server log. Next stage, unsubscribe links will too, and those never expire. The privacy stage now has to filter them out.
Me, using the app. The account menu did not close when I tapped outside it on my phone. Every test had passed. A person using it noticed.
Me, deciding. The plan suggested that the sign-in page should follow the browser’s language. I decided against a third source of language. Lite has one admin, so it knows whose language it is before anyone signs in.
Four checks, four different blind spots
The AI is fast and mostly right. That is what makes the mistakes easy to miss. The tests caught what the plan described. The review caught what the tests did not ask about. I caught what only shows up when a person holds the phone. And some calls were never errors at all, only choices, and those are mine to make.
None of these checks would have been enough alone.
The loop behind each stage is simple. The stage starts with a written plan, a small spec with acceptance criteria written as Gherkin scenarios, the way behaviour-driven development does it. Claude Code writes the tests first and builds against them: 53 tests and 12 browser tests, all checked against those scenarios. Then the separate review, and then I decide what gets fixed. The framework behind it will get its own post, probably after Stage 4.
Still not usable, and that is the plan
Stage 1 is tagged and released as v0.1-foundation on GitHub. There is still nothing to install for real use.
Stage 2 starts tomorrow: Amazon SES. It is a week without app code. A sending domain gets set up for deliverability and data protection, and at the end a test email sent from the command line has to land in the inbox at Gmail, GMX and Outlook, not in spam.
Every stage is on the roadmap. And if you build software yourself, tell me: what would you have left out of this stack, or added?