Skip to main content

Command Palette

Search for a command to run...

How I Think About Building Rails Apps in 2026

Updated
6 min read
How I Think About Building Rails Apps in 2026

I recently overhauled my Rails starter template. Not because anything was broken — the foundation was fine. But the landscape shifted, and some assumptions I'd been making needed revisiting.

This is how I think about it now. Opinions, not rules.

What I Started With

I needed a template for my personal projects. I considered Bullet Train, especially given I was a contributor there, but it was too heavy for my use case. I usually run 3-5 apps on the same 2GB server. Other templates are either not free or don't match how I like to build things. So I made my own.

The baseline: edge Rails from main branch — upgrades are atomic and automated with Dependabot, which helps avoid painful version migrations. SQLite for everything — database plus the Solid Trifecta (Cache, Queue, Cable). Faster than Postgres, production-ready, no Redis. Devise for authentication. ULID primary keys. Avo for admin. Hotwire instead of React or Vue. Tailwind for styling.

This foundation was solid. But 2025 changed some assumptions.

AI-Native by Default

The last six months brought massive progress in AI-assisted development. That progress pushed me to finally upgrade the template.

Agents are users now

Simple rule: if a human can do it through UI, an agent should be able to do it through an API.

This isn't hypothetical anymore. Internal automation scripts need programmatic access. AI assistants need to interact with your app. Integration tools expect standard protocols.

My implementation uses MCP (Model Context Protocol) as the transport. Every feature gets a model, a controller, and an MCP tool. The development rule: "Every new feature MUST have MCP parity."

Your app becomes automatable from day one. Retrofitting this later is painful.

RubyLLM integration

I built in RubyLLM with OpenAI and Anthropic support. Chat interface with message history. Cost tracking per conversation.

AI isn't an add-on anymore. If your app will eventually need AI features — and most will — build with that assumption from the start.

The chat system is central to any AI-enabled app. You need it for internal use: auto-generating summaries, processing content, running background AI tasks. The user-facing ChatGPT-style UI is a bonus that can be disabled if you don't need it.

LLM settings

The template includes configuration for Claude Code to work well with the codebase. Style guides for Ruby, testing, migrations, views, Stimulus, Tailwind, i18n — they load automatically and keep the AI aligned with project conventions. Specialized agents for Rails-specific tasks: code-reviewer, debugger, ui-designer. Slash commands like /review-code, /commit-push-pr, /favicon for quick access to common workflows.

This structure makes AI assistance consistent. The AI reads the same conventions every time, follows the same patterns, catches the same issues.

TDD matters more now

Tests first, implementation second. Minitest + fixtures. No RSpec, no FactoryBot.

This matters more than ever with AI-assisted development. TDD gives the AI structure. Tests define what "correct" means before the AI writes anything. Without that constraint, LLMs produce plausible-looking code that drifts from what you actually need. With tests, the AI has a target to hit and immediate feedback when it misses.

Justin Searls puts it well — there's an odd paradox in the industry right now: developers experienced in TDD are both the most skeptical of AI code generation AND the most successful at building software with coding agents. The skepticism comes from caring about quality. The success comes from having techniques that let agents verify their own work. Tests give the AI a target and immediate feedback. Without them, you're just hoping the output is correct.

Obie Fernandez makes a related point: the process defines the quality of the output; the tool just accelerates whatever process you already follow. If your process is sloppy, AI makes you sloppy faster. If your process includes tests, the AI has guardrails.

Vanilla Rails

37signals open-sourcing Fizzy was a good example of this approach in production — a real SaaS built without extra layers. For the first time, developers can study actual 37signals production code, not just read blog posts about their philosophy.

The approach: fat models, thin controllers. Concerns named as adjectives (Closeable, Publishable) instead of service objects. State as records, not booleans — card.closure instead of card.closed. Database constraints over model validations where possible.

I explicitly avoid service objects, query objects, form objects, Devise, Pundit, Sidekiq, Redis, ViewComponent, GraphQL, React/Vue/npm/yarn.

Fewer patterns means less cognitive load. Rails already provides enough. And there's another reason now: simpler stacks are easier for AI to work with. Martin Alderson's recent research shows Ruby ranks #3 in token efficiency across 19 languages — right after Clojure and Julia. Less boilerplate, fewer tokens, longer context windows, lower costs.

The old version used Devise. I removed it.

Passwords have become obsolete for most apps. Users forget them constantly. The recovery flow sends an email anyway. So the password step can be skipped entirely — just send magic links through email.

One less thing for users to manage. One less attack surface. Simpler code.

Worth considering 2FA later for apps that need it, but for my current projects it's not necessary.

Database Decisions

UUIDv7 replaces ULID

I moved from ULID to native UUIDv7. UUIDv7 is the industry standard now — time-ordered, globally unique, and the database generates them directly. No model callbacks needed.

id: { type: :string, default: -> { "uuid7()" } }

ULID was a good idea before UUIDv7 existed. Now there's no reason to use it.

Litestream for replication

Litestream handles automatic SQLite backups to cloud storage. Zero configuration in application code. Backup and replication used to be a real concern with SQLite — Litestream solves that completely.

Frontend Decisions

Hotwire stays

Server-rendered HTML first. JavaScript as sprinkles, not foundation. Progressive enhancement — works without JS.

Tailwind CSS v4

Tailwind v4 brings CSS-first configuration now. No JavaScript config file. And a strict rule: OKLCH colors only. Modern color space, better perception.

Server-centric rendering also has an unexpected benefit: agents don't need to parse JavaScript to understand your app structure. Server-rendered HTML is readable.

Developer Experience

Quality gates

bin/ci runs Rubocop + tests + Brakeman + i18n checks. Pre-commit hooks enforce this automatically. No exceptions — must pass before every commit.

Automation beats hoping for discipline.

Documentation

Comprehensive AGENTS.md that works for humans and AI assistants alike. Moved the template closer to Rails conventions where sensible.

Madmin over Avo

Switched from Avo to Madmin. I love Adrian, but some features I consider basic (sorting, dashboard graphs) require a premium subscription. For my personal projects that barely break even, Madmin fits better. It's lighter, closer to vanilla Rails, and does what I need without extra complexity.

What I'm Working On Next

Currently adding to the template: Stripe for subscriptions and payments, Evil Martians' LaunchKit — a well-researched landing page template designed for developer tools based on their analysis of 100+ devtool sites — and user settings for preferences and notifications.

Also upgrading my existing projects with these changes. First ListenWith.Me — it doesn't have production data to worry about, so it's a good testing ground. Then WhyRuby.info, which will require some tricks: moving from DigitalOcean to Hetzner (5x cheaper) and switching IDs from ULID to UUIDv7 in the same migration.


A modern Rails template should be AI-native. Not "AI-compatible" as an afterthought — native from the start. MCP tools alongside controllers. AGENTS.md alongside README. Tests that work for humans and agents alike.

The rest is vanilla Rails and simplicity. Every choice either removes complexity or adds genuine value.

Curious if this would work for you — or what you'd do differently.