BranchDb
Automatic per-branch PostgreSQL databases for Rails development.
BranchDb eliminates database migration conflicts by giving each git branch its own isolated database. Switch branches freely without worrying about schema mismatches or losing development data.
Table of Contents
- The Problem
- The Solution
- Installation
- Configuration
- Usage
- How It Works
- Requirements
- Important Notes
- Troubleshooting
- Development
- Roadmap
- Contributing
- License
The Problem
Working on multiple feature branches with different migrations causes pain:
# On feature-a branch: Add a 'status' column
rails generate migration AddStatusToUsers status:string
rails db:migrate
# Switch to feature-b branch
git checkout feature-b
git status
# => modified: db/schema.rb <- Contains 'status' column from feature-a!
# Now your schema.rb has changes that don't belong to this branch
# Accidentally commit it? You've just mixed schema changes across branches
# Run db:migrate? Schema.rb still shows the foreign column
More complex migrations could cause the code to stop working like renaming tables, adding foreign keys, or removing columns!
The Solution
BranchDb automatically manages separate databases for each branch:
main branch → myapp_development_main
feature-auth → myapp_development_feature_auth
feature-payments → myapp_development_feature_payments
bugfix-login → myapp_development_bugfix_login
Each branch has its own isolated database with its own schema and data. Switch branches, restart your server, and you're working with the right database automatically.
Installation
Add BranchDb to your Gemfile:
group :development, :test do
gem 'branch_db'
end
Install and run the generator:
bundle install
rails generate branch_db:install
Update your config/database.yml:
development:
<<: *default
database: <%= BranchDb.database_name('myapp_development') rescue 'myapp_development' %>
test:
<<: *default
database: <%= BranchDb.database_name('myapp_test') rescue 'myapp_test' %>
Note: The
rescuefallback ensures production/staging environments work correctly since the gem is only loaded in development/test. Replacemyappwith your application name.
Initialize your first branch database:
rails db:prepare
Configuration
The generator creates config/initializers/branch_db.rb:
BranchDb.configure do |config|
# The name of your main/stable branch (default: 'main')
config.main_branch = 'main'
# Maximum length for branch name suffix (default: 33)
# PostgreSQL has a 63 character limit for database names
# Formula: base_name_length + 1 (underscore) + max_branch_length <= 63
config.max_branch_length = 33
# Database name suffixes for cleanup feature (default: '_development', '_test')
# Customize if your database names use different conventions
# config.development_suffix = '_development'
# config.test_suffix = '_test'
end
Configuration Options
| Option | Default | Description |
|---|---|---|
main_branch |
'main' |
Your primary branch name (used as fallback clone source) |
max_branch_length |
33 |
Max characters for branch suffix (prevents exceeding PostgreSQL's 63-char limit) |
development_suffix |
'_development' |
Suffix pattern for development databases |
test_suffix |
'_test' |
Suffix pattern for test databases |
Usage
Daily Workflow
BranchDb enhances Rails' built-in db:prepare command:
# Just use Rails' standard command - now branch-aware!
rails db:prepare
What happens (development only):
- Checks if your branch's database exists and has schema
- If missing or empty and a parent/main DB exists, clones from the parent branch (or main as fallback)
- If on main branch or no source exists: defers to standard Rails behavior
- Rails then runs pending migrations and seeds as usual
Test databases use standard Rails behavior (schema load, no cloning):
RAILS_ENV=test rails db:prepare
Note: Cloning only runs in development environment. All commands support Rails' multiple database feature.
Available Commands
| Command | Description |
|---|---|
rails db:prepare |
Rails' standard command, enhanced with cloning from parent/main |
rails db:branch:list |
List all branch databases |
rails db:branch:purge |
Remove all branch databases except current and main. Use FORCE=1 to skip confirmation. |
rails db:branch:prune |
Remove databases for branches that no longer exist in git. Use FORCE=1 to skip confirmation. |
Examples
# Starting work on a new feature branch
git checkout -b feature-new-thing
rails db:prepare # Clones from parent branch (or main as fallback)
rails server
# Switching to another branch
git checkout feature-other-thing
# Restart your Rails server to connect to the other database
rails server
# Purging all branch databases (keeps current and main only)
rails db:branch:purge
# => Found 5 database(s) to remove:
# => - myapp_development_feature_old
# => - myapp_test_feature_old
# => ...
# => Proceed with deletion? [y/N]
# Pruning databases for deleted git branches only
rails db:branch:prune
# => Found 2 database(s) to remove:
# => - myapp_development_merged_feature
# => - myapp_test_merged_feature
# => Proceed with deletion? [y/N]
# Skip confirmation prompt (useful for CI/scripts)
FORCE=1 rails db:branch:purge
FORCE=1 rails db:branch:prune
How It Works
Rails Integration
BranchDb enhances Rails' db:prepare task by adding a prerequisite that clones from the parent branch when needed. That means:
- You keep running
rails db:prepare— no new commands to learn. - New branch databases are cloned from their parent, falling back to main.
- Migrations, seeds, and schema dumps work the way Rails normally handles them.
Database Naming
BranchDb generates database names by combining your base name with a sanitized branch name:
Base name: myapp_development
Branch: feature/user-auth
Sanitized: feature_user_auth
Result: myapp_development_feature_user_auth
Branch names are sanitized: non-alphanumeric characters become underscores, and names are truncated to max_branch_length.
Cloning Process
When db:prepare detects a missing or empty database:
- Detect the parent branch to clone from (see below).
- Check whether the parent database exists; fall back to main if it doesn't.
- If a source exists, create the target database and copy it with
pg_dump | psql. - If no source exists, defer to Rails' standard
db:prepare(schema load, migrations, seeds). - On the main branch, defer to Rails' standard
db:prepare.
Parent Branch Detection
BranchDb figures out which branch you branched from and clones that branch's database, so nested feature branches work:
main → feature-a → feature-a-child
When you create feature-a-child from feature-a, BranchDb will clone from feature-a's database (if it exists), not main.
Detection priority:
BRANCH_DB_PARENTenvironment variable (explicit override)- Git reflog analysis (finds the last "checkout: moving from X to current-branch")
- Configured
main_branch(fallback)
Fallback behavior: If the detected parent's database doesn't exist, BranchDb automatically falls back to the main branch database.
Override with environment variable:
# Force cloning from main, even if on a nested feature branch
BRANCH_DB_PARENT=main rails db:prepare
# Clone from a specific branch
BRANCH_DB_PARENT=feature-other rails db:prepare
Overriding the Branch or Database
For cases where you want to point the app at a different database than the auto-derived branch DB (teammate's branch DB, restored dump, shared QA DB, etc.), use one of:
Override the branch — everything else (branch suffix, clone source, purge protection) stays consistent, as if you were literally on that branch:
BRANCH_DB_BRANCH=main bin/rails server
⚠️ Warning:
BRANCH_DB_BRANCHmakes purge protection follow the override, not your real branch. If you setBRANCH_DB_BRANCH=mainand runrails db:branch:purge, your real branch's database will be treated like any other branch DB and dropped. Unset the variable before running destructive tasks.
Override the database name — bypasses branch-based logic entirely. Use one env var per Rails environment:
BRANCH_DB_DATABASE_DEVELOPMENT=shared_dev_db bin/rails server
BRANCH_DB_DATABASE_TEST=shared_test_db bin/rails test
When BRANCH_DB_DATABASE_<ENV> is set for the current Rails.env, the db:branch:* rake tasks (list, purge, prune, ensure_cloned) are skipped with a log message. BranchDb assumes you're managing the overridden database's lifecycle yourself.
⚠️ Single-database apps only. The database-name override dispatches by base-name suffix (e.g.,
_development,_test). If your app uses multiple PostgreSQL databases (e.g.,primaryandanalytics), two configs sharing the same suffix will all receive the same override value, which PostgreSQL cannot satisfy. UseBRANCH_DB_BRANCHinstead, or wait for per-connection override support.
Precedence: if both BRANCH_DB_BRANCH and BRANCH_DB_DATABASE_<ENV> are set, the database override wins for the connection name; the branch override still affects current_branch for anything else.
Purge Safety
The purge command protects important databases:
- Current branch's development and test databases
- Main branch's development and test databases
- Databases with active connections (skipped with warning)
Requirements
- Ruby >= 3.2
- Rails >= 7.0
- PostgreSQL (any supported version)
- PostgreSQL client tools in PATH:
psql- for database operationspg_dump- for cloning databasesdropdb- for purge/prune operations
Verifying PostgreSQL Tools
which psql pg_dump dropdb
# Should output paths for all three tools
If missing, install PostgreSQL client tools:
# macOS
brew install postgresql
# Ubuntu/Debian
sudo apt-get install postgresql-client
# Docker (add to your Dockerfile)
RUN apt-get update && apt-get install -y postgresql-client
Important Notes
Server Restart Required
Database selection happens at Rails boot time (ERB in database.yml is evaluated once). After switching branches, restart your Rails server to connect to the correct database.
git checkout other-branch
# Must restart Rails to use other-branch's database
rails server # Now connected to myapp_development_other_branch
Detached HEAD State
In detached HEAD state (e.g., git checkout abc123), BranchDb cannot determine a branch name. It falls back to using the base database name without a suffix. All detached HEAD checkouts share this database.
For CI environments, ensure you checkout an actual branch:
# CI script
git checkout $BRANCH_NAME # Not just the commit SHA
rails db:prepare
Database Name Length
PostgreSQL limits database names to 63 characters. With default settings:
- Base name: up to 29 characters
- Underscore: 1 character
- Branch suffix: up to 33 characters
If your base name is longer, reduce max_branch_length accordingly.
Troubleshooting
"PostgreSQL tool 'X' not found in PATH"
Install PostgreSQL client tools (see Requirements).
"Could not connect to Postgres on port X"
Ensure PostgreSQL is running and accessible:
# Check if PostgreSQL is running
pg_isready -h localhost -p 5432
# For Docker users
docker ps | grep postgres
Database not switching when I change branches
Remember to restart your Rails server after switching branches. The database name is determined at boot time.
Clone is slow for large databases
pg_dump | psql is already efficient, but for very large databases consider:
- Keeping your main branch database lean
- Using database-level compression
- Running cleanup regularly to remove old branch databases
Branch name too long
Long branch names are automatically truncated to max_branch_length (default: 33). Two branches with the same prefix might collide:
feature/very-long-descriptive-name-for-auth → _feature_very_long_descriptive_na
feature/very-long-descriptive-name-for-payments → _feature_very_long_descriptive_na # Same!
Use shorter branch names or increase max_branch_length (if your base name is short enough).
Development
Setup
git clone https://github.com/milkstrawai/branch_db.git
cd branch_db
bin/setup
Running Tests
# Run test suite
bundle exec rspec
# Run with coverage report
bundle exec rspec && open coverage/index.html
# Run linter
bundle exec rubocop
# Run both
bundle exec rake
Test Coverage
Coverage thresholds:
- Line coverage: 100%
- Branch coverage: 90%
Roadmap
Features we're considering for future releases:
- [ ] SQLite and MySQL support - Database adapter pattern for non-PostgreSQL databases
- [ ] Standalone clone task -
rails db:branch:clone FROM=branch-namefor manual cloning - [ ] Post-checkout git hook - Auto-restart Rails server on branch switch (Doable?)
- [ ] Database info task -
rails db:branch:infoshowing current branch, DB name, size, and parent - [ ] Clone progress indicator - Visual feedback for large database clones
- [ ] Disk usage report -
rails db:branch:list --sizeto show storage per branch
Have a feature request? Open an issue to discuss it!
Contributing
Contributions are welcome. The usual drill:
- Fork the repository.
- Create a feature branch:
git checkout -b feature/amazing-feature. - Commit your changes:
git commit -m 'Add amazing feature'. - Push the branch:
git push origin feature/amazing-feature. - Open a pull request.
Guidelines
- Write tests for new features
- Follow existing code style (RuboCop will help)
- Update documentation as needed
- Keep commits focused and atomic
Reporting Issues
Found a bug? Please open an issue with:
- Ruby and Rails versions
- PostgreSQL version
- Steps to reproduce
- Expected vs actual behavior
License
BranchDb is available as open source under the terms of the MIT License.
Acknowledgments
Inspired by the pain of database migration conflicts and the joy of isolated development environments.