Updating RailsFast
Updating any template is always going to be a bit difficult, because as you start building your app on top of it many files will diverge from the original template, and conflicts will naturally arise. The worst-case scenario is you need to go file-by-file reviewing the template updates and deciding what to incorporate into your app.
HOWEVER, RailsFast is designed so the amount of effort you need to do is minimized, and ideally you can keep pulling RailsFast improvements into your app without much work.
This guide was battle-tested by upgrading a whole fleet of real production apps — including apps forked back on v0.x — with AI agents driving every merge, each one logging every snag. Everything below reflects what those upgrades actually hit.
Make your life easier for updating
As we covered in the Customizing Components section, the best practice if you plan to customize RailsFast components, is that you don't edit the RailsFast components in-place, instead, copy the component you want to change into your own folder and edit your copy.
Same idea for gems: the Gemfile ends with a "GEMS SPECIFIC TO THIS PROJECT" section — put your app's own gems there, not inside the template-owned regions, and your Gemfile stops conflicting on every future update.
Protect your customizations (merge guards)
At some point, you will inevitably edit RailsFast core files. If you did edit files that ship with RailsFast and that you can't copy easily into your own folder (for example, layouts or auth views), you can tell Git to always keep your version on merges by adding merge guards in .gitattributes.
Example:
# Keep my customized files when merging template updates
app/views/devise/** merge=ours
This way, if for example you update all your authentication screens to match your branding and design, when updating RailsFast your files will get priority, overriding the update, and you won't lose your work (you won't get updates on those files, though)
The .gitattributes line alone does nothing. ours is not a built-in Git merge driver — the guard only works when the driver is configured:
git config merge.ours.driver true
bin/setup configures this for you, but a fresh clone on a new machine (or a CI checkout) doesn't have it — and then Git silently falls back to a normal merge on your "protected" files, in the direction of losing your customizations. Verify before any merge:
git config --get merge.ours.driver # must print: true
Three more truths about guards, each learned on a real upgrade:
- Guards are silent. A guarded file that kept your version prints an ordinary
Auto-mergingline — nothing tells you an upstream improvement was just discarded. That's why diffing guarded files is a standing step in After updating, not a v0.x special. - Guards don't cover deletions. Merge drivers run only for content conflicts. If upstream deletes a file you've modified, you get a raw
CONFLICT (modify/delete)that no guard resolves. Hard rule for that moment: on any conflict touchingcredentials.yml.enc,master.key, or any.key/encrypted file — keep YOUR version, always. The template cleaning up its own sample file is never a reason to delete your live credentials. - Guard only what you must. If you guard huge paths like
app/views/**, you will also silently skip upstream improvements (including fixes) for those files.
Before you update: figure out where you are
Three checks, one minute, and they prevent the worst classes of mistake:
# 1. Is this repo actually a fork of the template? (directory names lie)
git remote -v # is the railsfast remote there?
git fetch railsfast --prune --tags # fetch FIRST — and bring the release tags
git merge-base HEAD railsfast/main # a SHA = real shared ancestry; an error = STOP
# 2. Where did you *really* fork? (version constants on old apps can lie)
git tag --contains $(git merge-base HEAD railsfast/main) | head -3
# 3. Establish your baseline — run the gate BEFORE merging:
bin/ci
Fetch before you size up the job: measuring against a stale railsfast/main ref can overestimate the work several-fold. And the pre-merge bin/ci run is what lets you say "the merge broke this" versus "this was already broken" — if main is already red, fix that first, or you'll debug two problems tangled together.
How to update RailsFast
Work on a branch — never merge the template directly into main on a production app:
git switch -c chore/template-update
Then merge. Pinning a release is the reliable way — merging railsfast/main gets you whatever has landed since the last release, which can be more than you bargained for:
git tag --list 'v*' --sort=-v:refname | head -5 # what releases exist?
git merge v1.0.0-beta.4 # pin a release (recommended)
# or: git merge railsfast/main # bleeding edge, eyes open
If you get conflicts, resolve them (Git will respect your merge guards — assuming the driver check above passed), and commit the merge. bin/railsfast update prints this whole flow tailored to your repo.
Generated files: regenerate, don't hand-merge
db/schema.rb and Gemfile.lock conflict on almost every non-trivial update, and the correct resolution is never a careful hand-merge:
Gemfile.lock: resolve theGemfileconflict first, thenbundle install— the lockfile regenerates itself.db/schema.rb: take your side, thenbin/rails db:migrateapplies the template's new migrations and re-dumps the schema authoritatively. Free correctness check: if you did hand-union it first, a zero diff afterdb:migrateproves your whole resolution was right.
Port what the guards kept out
Guarded files keep your copy wholesale — which also means new template keys and improvements never arrive in them on their own. After merging, diff every guarded path against the release you merged, and port what's missing:
# the guard list lives in .gitattributes; for each merge=ours path:
diff <(git show v1.0.0-beta.4:config/railsfast/railsfast.yml) config/railsfast/railsfast.yml
diff <(git show v1.0.0-beta.4:config/application.rb) config/application.rb
diff <(git show v1.0.0-beta.4:.kamal/secrets) .kamal/secrets
# ...and so on down the .gitattributes list
This is mechanical, and it's the step that catches a new config key or an improved secrets expression that the guard silently discarded. Watch for nested keys — check paths, not just top-level names.
Using AI agents to resolve conflicts
Merge conflicts are inevitable when you've customized template files and the upstream template has improvements to those same files. The good news: AI coding agents like Claude Code excel at resolving these conflicts intelligently — the fleet of real-app upgrades that road-tested this guide was agent-driven end to end.
Give the agent the whole protocol, not just the merge:
Update this app to RailsFast <release tag>, on a branch, following
https://railsfast.com/docs/guides-updating/ exactly: verify
merge.ours.driver is configured, run a baseline bin/ci first, merge the
release tag, resolve conflicts (keep my customizations in app-owned
files; regenerate schema.rb and Gemfile.lock instead of hand-merging;
on anything touching credentials or .key files keep mine), diff every
merge-guarded file against the release and port missing keys, get
bin/ci green, run bin/railsfast doctor, then open a PR.
An agent given that prompt does the right thing. An agent told only to "merge and resolve conflicts, keep my styling" will skip exactly the steps this page says the merge cannot do by itself.
Enable Git's rerere ("reuse recorded resolution") before merging — it records how you resolve each conflict and replays it if you ever redo the merge: git config rerere.enabled true
After updating
In order — every step is here because skipping it bit a real upgrade:
bundle install # the update may have changed the Gemfile
bin/rails db:migrate # new template migrations
bin/ci # THE gate: style, audits, Brakeman, all tests, seeds
bin/railsfast doctor # production readiness — every finding carries its fix
bin/railsfast mark-updated # record the update so `status` tracks drift correctly
Also diff config/bundler-audit.yml against the release: if the template ships an advisory ignore, check whether a patched gem has shipped since — inheriting a suppression when the patch exists means you're suppressing, not patched.
Then restart your dev server (bin/dev), click around, and land your update branch via PR like any other change.
What each release needs from you
- v1.0.0-beta.4: nothing manual —
stripe.context(Stripe Organizations support) is opt-in, the backups3_endpoint.keyis optional, no migrations. If yourconfig/bundler-audit.ymlcarries apayadvisory ignore, bump pay to ≥ 11.6.2 and drop it. - v1.0.0-beta.3: the agent surface arrives (
bin/railsfast doctor, the MCP server, the bundled Claude skill) — no manual steps. - From v0.x: the big jump — next section.
Upgrading to v1.0.0-beta (from v0.x)
The v1.0.0 betas are a big jump (Ruby 4, sessions & device security, the native server half, dark mode, organizations 0.5). A fleet of real apps has crossed it — the demo, railsfast.com itself, and several production apps.
Run bin/railsfast doctor first, not last. Old apps sometimes moved ahead independently (already on Ruby 4, already generated encryption keys) — doctor turns this checklist from "do all of this" into "verify, then act on what's actually missing":
- Active Record encryption keys are required in production.
config/application.rbfails closed on boot without them (fresh apps get them frombin/setup; upgraded apps don't — and doctor reports it asar-encryption-keys). Generate and add them BEFORE deploying, or your first deploy dies at the health check:bin/rails db:encryption:init # copy the printed block into: EDITOR="cursor --wait" bin/rails credentials:edit - Diff every merge-guarded file against the release and port what's missing — the mechanical step above, which stays accurate as releases move (a hardcoded key list here would rot). One v0.x-specific find the diff will surface: delete the stale
template_version:key fromconfig/railsfast/railsfast.yml— the version's single source of truth moved tolib/railsfast/version.rb, and the guarded yml would keep the dead key forever. - Ruby 4.0.5:
rbenv install 4.0.5/mise install, thenbundle install. If you use a remote builder, updateRUBY_VERSIONinconfig/deploy.yml's builder args — and check your Dockerfile'sARG RUBY_VERSIONif you've customized it. bin/rails db:migrate(sessions tables + organizations 0.5).
If you customized Stimulus-driven markup: the template reorganized all controllers under railsfast/ and app/ ownership folders, so old root-level identifiers (navbar, dropdown, slideover, …) are now railsfast--layout--navbar, railsfast--ui--dropdown, and so on — update any data-controller attributes in views you own.