[{"data":1,"prerenderedAt":730},["ShallowReactive",2],{"\u002Fblog\u002Frails-database-per-branch-workflow-branchdb":3},{"path":4,"data":5,"meta":17,"nodes":57},"\u002Fblog\u002Frails-database-per-branch-workflow-branchdb",{"title":6,"description":7,"date":8,"tags":9,"authors":11,"image":13,"seo":14},"Stop Fighting Your Database When Switching Git Branches","Database-per-branch workflow in Ruby on Rails using the BranchDb gem","2026-02-04",[10],"engineering",[12],"Ali Fadel","\u002Fimages\u002Fblog\u002Frails-database-per-branch-workflow-branchdb.webp",{"title":15,"description":16},"Rails Database-per-Branch: Automated Isolated Environments with BranchDb","Tired of schema bleed and migration conflicts? Discover how to automate a database-per-branch workflow in Ruby on Rails using the BranchDb gem.",{"toc":18,"key":49,"source":50,"stem":51,"extension":52,"kind":53,"type":54,"partial":55,"hash":56},{"title":19,"searchDepth":20,"depth":20,"links":21},"",2,[22,25,28,31,34,37,40,43,46],{"id":23,"depth":20,"text":24},"the-obvious-solution-nobody-uses","The Obvious Solution Nobody Uses",{"id":26,"depth":20,"text":27},"making-it-automatic","Making It Automatic",{"id":29,"depth":20,"text":30},"how-it-actually-works","How It Actually Works",{"id":32,"depth":20,"text":33},"the-cleanup-problem","The Cleanup Problem",{"id":35,"depth":20,"text":36},"what-this-changes","What This Changes",{"id":38,"depth":20,"text":39},"the-tradeoffs","The Tradeoffs",{"id":41,"depth":20,"text":42},"getting-started","Getting Started",{"id":44,"depth":20,"text":45},"whats-next","What's Next",{"id":47,"depth":20,"text":48},"the-bigger-point","The Bigger Point","blog\u002Frails-database-per-branch-workflow-branchdb.md","blog","rails-database-per-branch-workflow-branchdb",".md","document","text\u002Fmarkdown",false,"7bc28ceba871a795",[58,68,76,79,86,89,95,98,137,140,143,146,253,257,270,277,304,311,318,321,332,338,342,349,363,373,401,404,407,411,414,417,420,426,432,438,448,452,455,458,461,464,467,471,474,480,502,516,520,563,584,589,625,631,642,646,649,659,669,679,688,697,705,709,712,715,718,721,724,727],[59,60,61,62,67],"p",{},"Here's something that happens to every ",[63,64,66],"a",{"href":65},"https:\u002F\u002Frubyonrails.org\u002F","Rails"," developer eventually.",[59,69,70,71,75],{},"You're working on a feature branch. You add a migration, run it, everything works. Then your teammate asks you to review their PR. You switch branches. Suddenly your ",[72,73,74],"code",{},"schema.rb"," shows changes you didn't make. Changes from your feature branch are now bleeding into their branch.",[59,77,78],{},"You know you shouldn't commit it. But now you're in a weird state. Your database has columns that don't exist in this branch's code. Things break in confusing ways.",[59,80,81,82,85],{},"Most developers just deal with it. They run ",[72,83,84],{},"db:migrate",", hope for the best, maybe manually roll back migrations. Some teams have elaborate wiki pages explaining the \"correct\" way to switch branches. None of it works reliably.",[59,87,88],{},"The actual problem is simple: one database, many branches. That's a fundamental mismatch.",[90,91,92],"h2",{"id":23},[93,94,24],"strong",{},[59,96,97],{},"What if each branch had its own database?",[99,100,103],"pre",{"language":101,"class":102},"jsx","shiki shiki-themes github-light-default github-dark-default dark:github-dark-default",[72,104,106,115,116,115,128],{"class":105},"language-jsx",[107,108,111],"span",{"class":109,"style":110},"line","display: inline",[107,112,114],{"style":113},"color:#1F2328;--shiki-dark:#E6EDF3","main branch      → myapp_development_main","\n",[107,117,118,121,125],{"class":109,"style":110},[107,119,120],{"style":113},"feature",[107,122,124],{"style":123},"color:#CF222E;--shiki-dark:#FF7B72","-",[107,126,127],{"style":113},"auth     → myapp_development_feature_auth",[107,129,130,132,134],{"class":109,"style":110},[107,131,120],{"style":113},[107,133,124],{"style":123},[107,135,136],{"style":113},"payments → myapp_development_feature_payments",[59,138,139],{},"Switch branches, restart your server, you're using a completely isolated database. No schema bleed. No migration conflicts. No weird states.",[59,141,142],{},"This isn't a new idea. Developers have been manually creating separate databases for years. But it's tedious. You have to remember to create the database, clone the data, keep track of which branch uses which database. So people don't do it.",[59,144,145],{},"The friction kills the solution.",[147,148,149,171],"table",{},[150,151,152],"thead",{},[153,154,155,161,166],"tr",{},[156,157,158],"th",{},[93,159,160],{},"Feature",[156,162,163],{},[93,164,165],{},"The \"Old Way\" (Single Shared DB)",[156,167,168],{},[93,169,170],{},"The \"BranchDb Way\" (Isolated DBs)",[172,173,174,188,201,214,227,240],"tbody",{},[153,175,176,182,185],{},[177,178,179],"td",{},[93,180,181],{},"Migration State",[177,183,184],{},"Bleeds between branches",[177,186,187],{},"Fully isolated per branch",[153,189,190,195,198],{},[177,191,192],{},[93,193,194],{},"Schema.rb Integrity",[177,196,197],{},"Shows ghost changes from other branches",[177,199,200],{},"Always matches your current code",[153,202,203,208,211],{},[177,204,205],{},[93,206,207],{},"Destructive Testing",[177,209,210],{},"Risky; affects all work",[177,212,213],{},"Safe; stays within one branch",[153,215,216,221,224],{},[177,217,218],{},[93,219,220],{},"Context Switching",[177,222,223],{},"High mental overhead",[177,225,226],{},"Zero effort; \"just works\"",[153,228,229,234,237],{},[177,230,231],{},[93,232,233],{},"Disk Usage",[177,235,236],{},"Minimal (one DB)",[177,238,239],{},"Higher (clones for each branch)",[153,241,242,247,250],{},[177,243,244],{},[93,245,246],{},"Tooling Required",[177,248,249],{},"Manual rollbacks & wiki pages",[177,251,252],{},"Automated Ruby gem",[90,254,255],{"id":26},[93,256,27],{},[59,258,259,260,264,265,269],{},"We built ",[63,261,263],{"href":262},"https:\u002F\u002Fgithub.com\u002Fmilkstrawai\u002Fbranch_db","BranchDb"," at ",[63,266,268],{"href":267},"https:\u002F\u002Fmilkstraw.ai\u002F","MilkStraw AI"," to remove that friction entirely.",[59,271,272,273,276],{},"Add it to your Gemfile. Run the generator. Update your ",[72,274,275],{},"database.yml"," with one line of ERB. That's the setup.",[99,278,280],{"language":279,"class":102},"yaml",[72,281,283,115,292],{"class":282},"language-yaml",[107,284,285,289],{"class":109,"style":110},[107,286,288],{"style":287},"color:#116329;--shiki-dark:#7EE787","development",[107,290,291],{"style":113},":",[107,293,294,297,300],{"class":109,"style":110},[107,295,296],{"style":287},"  database",[107,298,299],{"style":113},": ",[107,301,303],{"style":302},"color:#0A3069;--shiki-dark:#A5D6FF","\u003C%= BranchDb.database_name('myapp_development') rescue 'myapp_development' %>",[59,305,306,307,310],{},"The ",[72,308,309],{},"rescue"," guard matters. BranchDb only loads in development\u002Ftest, so production needs a fallback. Without it, your app won't boot in production.",[59,312,313,314,317],{},"Now when you run ",[72,315,316],{},"rails db:prepare"," on a new branch, it automatically clones from your parent branch. Not from main (unless you branched from main). From wherever you actually branched from. This cloning only happens for development databases. Test databases use standard Rails schema loading.",[59,319,320],{},"This matters more than you'd think.",[59,322,323,324,327,328,331],{},"Say you're working on ",[72,325,326],{},"feature-auth",". You've added users, set up test data, run several migrations. Now you need to branch off to fix a bug in that feature. You create ",[72,329,330],{},"feature-auth-hotfix",".",[59,333,334,335,337],{},"With BranchDb, your new branch gets a clone of ",[72,336,326],{},"'s database. All your test data, all your migrations, ready to go. You're not starting from a bare main branch and re-running everything.",[90,339,340],{"id":29},[93,341,30],{},[59,343,344,345,348],{},"The core is simple. BranchDb hooks into Rails' ",[72,346,347],{},"db:prepare"," task. When that task runs, it checks:",[350,351,352,356],"ol",{},[353,354,355],"li",{},"Does this branch's database exist?",[353,357,358,359,362],{},"Does it have a ",[72,360,361],{},"schema_migrations"," table?",[59,364,365,366,369,370,331],{},"If no to either, it looks for a source to clone from. First it checks ",[72,367,368],{},"git reflog"," to find your parent branch. Reflog analysis isn't perfect. Shallow clones in CI or complex rebase histories can break detection. When that happens, or when the detected parent's database doesn't exist, it falls back to main. You can also force a specific parent with ",[72,371,372],{},"BRANCH_DB_PARENT=some-branch rails db:prepare",[59,374,375,376,380,381,385,386,390,391,394,395,397,398,331],{},"Then it uses ",[63,377,379],{"href":378},"https:\u002F\u002Fwww.postgresql.org\u002F","PostgreSQL","'s native tools ",[63,382,384],{"href":383},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fapp-pgdump.html","pg_dump"," and ",[63,387,389],{"href":388},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fapp-psql.html","psql"," (",[72,392,393],{},"pg_dump | psql","). Fast, reliable, streaming the output directly so it doesn't consume RAM. Because it clones the actual database (not your schema file), it captures everything: tables, indexes, custom types, extensions. Works the same whether you use ",[72,396,74],{}," or ",[72,399,400],{},"structure.sql",[59,402,403],{},"If you're on main, or if no source database exists, it just defers to Rails' normal behavior. Load schema, run migrations, seed. Nothing magical.",[59,405,406],{},"The whole thing is under 400 lines of Ruby.",[90,408,409],{"id":32},[93,410,33],{},[59,412,413],{},"Branches pile up. So do databases.",[59,415,416],{},"After a few weeks you might have twenty branch databases sitting in PostgreSQL. Most for branches you've already merged and deleted.",[59,418,419],{},"BranchDb gives you three commands:",[59,421,422,425],{},[72,423,424],{},"rails db:branch:list"," lists all branch databases.",[59,427,428,431],{},[72,429,430],{},"rails db:branch:prune"," removes databases for git branches that no longer exist. If you deleted the branch, it deletes the database.",[59,433,434,437],{},[72,435,436],{},"rails db:branch:purge"," is more aggressive. It removes everything except your current branch and main.",[59,439,440,441,385,444,447],{},"Both ",[72,442,443],{},"prune",[72,445,446],{},"purge"," ask for confirmation. Both skip databases with active connections. Both protect your current work.",[90,449,450],{"id":35},[93,451,36],{},[59,453,454],{},"The shift is subtle but significant.",[59,456,457],{},"Before: switching branches is a context-loading exercise. You're thinking about database state, migration order, whether you need to reset anything.",[59,459,460],{},"After: switching branches is just switching branches. The database follows automatically.",[59,462,463],{},"You stop treating your development database as precious. Need to test something destructive? Just do it. Switch to another branch and you're back to clean state. Switch back and your destructive changes are still there, isolated.",[59,465,466],{},"It sounds small. But removing that mental overhead adds up. Every branch switch that doesn't require database thinking is a small win. Those wins compound.",[90,468,469],{"id":38},[93,470,39],{},[59,472,473],{},"Nothing is free.",[59,475,476,479],{},[93,477,478],{},"Disk space"," is the obvious one. Each branch database is a full copy. If your development database is 500MB, ten branches means 5GB. For most projects this is fine. Disk is cheap and you can prune regularly.",[59,481,482,485,486,488,489,397,492,496,497,501],{},[93,483,484],{},"Server restarts"," are required. The database name is determined when Rails boots (ERB in ",[72,487,275],{}," evaluates once). Switch branches without restarting and you're still connected to the old database. This catches people at first. If you're using ",[72,490,491],{},"bin\u002Fdev",[63,493,495],{"href":494},"https:\u002F\u002Fgithub.com\u002FDarthSim\u002Fovermind","Overmind",", the restart is quick. Some teams wire up a ",[63,498,500],{"href":499},"https:\u002F\u002Fgit-scm.com\u002Fdocs\u002Fgithooks#_post_checkout","post-checkout git hook"," to restart automatically. You get used to it either way.",[59,503,504,507,508,385,510,512,513,515],{},[93,505,506],{},"PostgreSQL only."," The cloning uses ",[72,509,384],{},[72,511,389],{},". Supporting other databases would mean different tooling for each. PostgreSQL covers most Rails projects, so that's where we started. If you're running Postgres in Docker, it just works. BranchDb talks to whatever Postgres your ",[72,514,275],{}," points to. No special volume handling needed.",[90,517,518],{"id":41},[93,519,42],{},[99,521,523],{"language":522,"class":102},"ruby",[72,524,526,115,532,115,550,115,558],{"class":525},"language-ruby",[107,527,528],{"class":109,"style":110},[107,529,531],{"style":530},"color:#6E7781;--shiki-dark:#8B949E","# Gemfile",[107,533,534,537,541,544,547],{"class":109,"style":110},[107,535,536],{"style":113},"group ",[107,538,540],{"style":539},"color:#0550AE;--shiki-dark:#79C0FF",":development",[107,542,543],{"style":113},", ",[107,545,546],{"style":539},":test",[107,548,549],{"style":123}," do",[107,551,552,555],{"class":109,"style":110},[107,553,554],{"style":113},"  gem ",[107,556,557],{"style":302},"'branch_db'",[107,559,560],{"class":109,"style":110},[107,561,562],{"style":123},"end",[99,564,565],{"language":101,"class":102},[72,566,567,115,572],{"class":105},[107,568,569],{"class":109,"style":110},[107,570,571],{"style":113},"bundle install",[107,573,574,577,581],{"class":109,"style":110},[107,575,576],{"style":113},"rails generate ",[107,578,580],{"style":579},"color:#953800;--shiki-dark:#FFA657","branch_db",[107,582,583],{"style":113},":install",[59,585,586,587,291],{},"Update ",[72,588,275],{},[99,590,591],{"language":279,"class":102},[72,592,593,115,599,115,607,115,609,115,616],{"class":282},[107,594,595,597],{"class":109,"style":110},[107,596,288],{"style":287},[107,598,291],{"style":113},[107,600,601,603,605],{"class":109,"style":110},[107,602,296],{"style":287},[107,604,299],{"style":113},[107,606,303],{"style":302},[107,608],{"class":109,"style":110},[107,610,611,614],{"class":109,"style":110},[107,612,613],{"style":287},"test",[107,615,291],{"style":113},[107,617,618,620,622],{"class":109,"style":110},[107,619,296],{"style":287},[107,621,299],{"style":113},[107,623,624],{"style":302},"\u003C%= BranchDb.database_name('myapp_test') rescue 'myapp_test' %>",[59,626,627,628,630],{},"Run ",[72,629,316],{},". You now have a branch-specific database.",[59,632,633,634,637,638,641],{},"That's it. No configuration required for most projects. If your main branch is called ",[72,635,636],{},"master"," instead of ",[72,639,640],{},"main",", there's a config option for that. If you need shorter database names, you can limit the branch suffix length. But the defaults work for most teams.",[90,643,644],{"id":44},[93,645,45],{},[59,647,648],{},"BranchDb solves the core problem, but there's more we want to build.",[59,650,651,654,655,658],{},[93,652,653],{},"SQLite and MySQL support."," Right now it's PostgreSQL only. We want to abstract the cloning layer so the same workflow works regardless of your database. SQLite would be trivial (it's just a file copy). MySQL needs ",[72,656,657],{},"mysqldump"," integration.",[59,660,661,664,665,668],{},[93,662,663],{},"Standalone clone task."," Sometimes you want to manually clone from a specific branch: ",[72,666,667],{},"rails db:branch:clone FROM=feature-x",". Useful when the automatic parent detection isn't what you need.",[59,670,671,674,675,678],{},[93,672,673],{},"Database info task."," ",[72,676,677],{},"rails db:branch:info"," to show your current branch, database name, size on disk, and detected parent. Quick sanity check without digging through configs.",[59,680,681,684,685,687],{},[93,682,683],{},"Clone progress indicator."," For large databases, ",[72,686,393],{}," can take a while. Visual feedback would help.",[59,689,690,674,693,696],{},[93,691,692],{},"Disk usage report.",[72,694,695],{},"rails db:branch:list --size"," to see how much space each branch database is consuming. Makes cleanup decisions easier.",[59,698,699,700,704],{},"If any of these would help your workflow, ",[63,701,703],{"href":702},"https:\u002F\u002Fgithub.com\u002Fmilkstrawai\u002Fbranch_db\u002Fissues","open an issue",". We're building based on what teams actually need.",[90,706,707],{"id":47},[93,708,48],{},[59,710,711],{},"Developer tools should remove friction, not add process.",[59,713,714],{},"The database-per-branch pattern isn't new. What's new is making it automatic. Making it the default behavior rather than something you have to remember to do.",[59,716,717],{},"Most developers won't manually create branch databases. The friction is too high for a problem that feels minor (until it isn't). But if the right thing happens automatically? Everyone benefits.",[59,719,720],{},"That's the bar for developer tooling. Not \"possible if you follow these steps.\" Automatic. Invisible. Just works.",[59,722,723],{},"BranchDb tries to meet that bar. You install it, update one config file, and forget about it. Branch databases just happen. Cleanup is one command away.",[59,725,726],{},"The best tools disappear into your workflow. You stop noticing them because they stop causing problems.",[59,728,729],{},"That's the goal.",1794319532577]