
Migrating from Jumpstart Pro to Bullet Train
Move from Jumpstart Pro's paid license to Bullet Train's open-source core while maintaining your Rails SaaS features.
Prerequisites
- •Ruby on Rails 7+ experience
- •Understanding of Hotwire/Turbo
- •Familiarity with your current Jumpstart Pro setup
Migrating from Jumpstart Pro to Bullet Train
Both Jumpstart Pro and Bullet Train are excellent Rails SaaS frameworks. You might consider migrating to Bullet Train if you prefer open-source, want different UI opinions, or need its specific scaffolding approach.
Why Migrate?#
Consider Bullet Train if:
- You prefer an open-source foundation
- You want its specific CRUD scaffolding generators
- The Bullet Train UI/UX patterns fit your vision
- You need more customization freedom
Stay with Jumpstart Pro if:
- You're happy with the subscription model
- You rely heavily on Jumpstart-specific gems
- Your team is already trained on Jumpstart patterns
- The migration cost exceeds the benefits
Architecture Comparison#
| Aspect | Jumpstart Pro | Bullet Train |
|---|---|---|
| License | Paid subscription | Open-source + Pro add-ons |
| UI Framework | Tailwind + custom | Tailwind + custom theme |
| JS Framework | Hotwire/Turbo | Hotwire/Turbo |
| Multi-tenancy | Accounts | Teams |
| Scaffolding | Standard Rails | Super Scaffolding |
| Billing | Pay gem | Pro add-on |
Step 1: Set Up Bullet Train#
# Create new Bullet Train project
git clone https://github.com/bullet-train-co/bullet_train.git my-app-bt
cd my-app-bt
# Install dependencies
bundle install
yarn install
# Setup database
rails db:create db:migrate
# Start the app
bin/dev
Step 2: Understand the Terminology#
| Jumpstart Pro | Bullet Train | Notes |
|---|---|---|
| Account | Team | Main organizational unit |
| AccountUser | Membership | User's role in organization |
| account_id | team_id | Foreign key reference |
| current_account | current_team | Helper method |
Step 3: Migrate Your Database Schema#
Create a Mapping Migration#
# db/migrate/XXXXXX_migrate_from_jumpstart.rb
class MigrateFromJumpstart < ActiveRecord::Migration[7.1]
def up
# Map accounts to teams
execute <<-SQL
INSERT INTO teams (id, name, slug, created_at, updated_at)
SELECT id, name, LOWER(REPLACE(name, ' ', '-')), created_at, updated_at
FROM accounts;
SQL
# Map account_users to memberships
execute <<-SQL
INSERT INTO memberships (id, user_id, team_id, role, created_at, updated_at)
SELECT
id,
user_id,
account_id,
CASE
WHEN owner = true THEN 'admin'
ELSE 'member'
END,
created_at,
updated_at
FROM account_users;
SQL
end
def down
# Reverse migration if needed
end
end
Export Data for Complex Migrations#
# scripts/export_jumpstart_data.rb
require 'json'
data = {
users: User.all.as_json,
accounts: Account.all.as_json,
account_users: AccountUser.all.as_json,
subscriptions: Subscription.all.as_json,
# Add your custom models
}
File.write('jumpstart_export.json', JSON.pretty_generate(data))
Step 4: Migrate Models#
User Model#
# Jumpstart Pro User
class User < ApplicationRecord
has_many :account_users, dependent: :destroy
has_many :accounts, through: :account_users
def personal_account
accounts.find_by(personal: true)
end
end
# Bullet Train User
class User < ApplicationRecord
has_many :memberships, dependent: :destroy
has_many :teams, through: :memberships
# Bullet Train uses scaffolding for team access
include Teams::Base
end
Custom Models Migration#
For each custom model in your Jumpstart app:
# Jumpstart: belongs to account
class Project < ApplicationRecord
belongs_to :account
belongs_to :user
end
# Bullet Train: belongs to team
class Project < ApplicationRecord
belongs_to :team
belongs_to :user, optional: true
# Use Bullet Train's concerns
include Teamable
end
Step 5: Migrate Controllers#
Current Account vs Current Team#
# Jumpstart Pro pattern
class ProjectsController < ApplicationController
before_action :authenticate_user!
before_action :set_account
def index
@projects = current_account.projects
end
private
def set_account
@account = current_user.accounts.find(params[:account_id])
end
end
# Bullet Train pattern
class ProjectsController < ApplicationController
account_load_and_authorize_resource :project, through: :team
def index
# @projects is automatically scoped to current team
end
end
Super Scaffolding#
Bullet Train's Super Scaffolding generates complete CRUD:
# Generate a new resource
bin/super-scaffold crud Project Team name:text_field description:trix_editor status:buttons
# This creates:
# - Model with team association
# - Controller with proper authorization
# - Views with Bullet Train UI components
# - Tests
Step 6: Migrate Views#
Layout Changes#
<%# Jumpstart Pro layout %>
<%= render "shared/navbar" %>
<div class="container mx-auto px-4">
<%= yield %>
</div>
<%# Bullet Train layout %>
<%= render "shared/header" %>
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<%= yield %>
</div>
Component Mapping#
| Jumpstart Component | Bullet Train Component |
|---|---|
render "form_field" | <%= render 'shared/fields/...' %> |
| Modal partials | Bullet Train modals |
| Flash messages | Bullet Train toasts |
Form Fields Example#
<%# Jumpstart Pro form %>
<%= form_with model: @project do |f| %>
<div class="form-group">
<%= f.label :name %>
<%= f.text_field :name, class: "form-control" %>
</div>
<% end %>
<%# Bullet Train form %>
<%= form_with model: @project do |f| %>
<%= render 'shared/fields/text_field', form: f, method: :name %>
<%= render 'shared/fields/trix_editor', form: f, method: :description %>
<% end %>
Step 7: Migrate Authentication#
Both use Devise, but configurations differ:
# Jumpstart Pro may have custom Devise modules
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:omniauthable, :masqueradable
end
# Bullet Train Devise config
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:trackable, :omniauthable
# Bullet Train uses Doorkeeper for API auth
has_many :access_tokens, class_name: 'Doorkeeper::AccessToken'
end
OAuth Providers#
# config/initializers/devise.rb
# Jumpstart Pro pattern
config.omniauth :twitter, ENV["TWITTER_CLIENT_ID"], ENV["TWITTER_CLIENT_SECRET"]
config.omniauth :google_oauth2, ENV["GOOGLE_CLIENT_ID"], ENV["GOOGLE_CLIENT_SECRET"]
# Bullet Train - same config, different callbacks
config.omniauth :twitter, ENV["TWITTER_CLIENT_ID"], ENV["TWITTER_CLIENT_SECRET"],
callback_path: '/users/auth/twitter/callback'
Step 8: Migrate Billing#
Jumpstart Pro uses Pay gem#
# Jumpstart billing
class Account < ApplicationRecord
pay_customer
def subscribed?
payment_processor&.subscribed?
end
end
# Usage
current_account.payment_processor.subscribe(plan: "pro")
Bullet Train Pro Billing#
# Bullet Train billing (requires Pro add-on)
class Team < ApplicationRecord
include Billing::Subscribable
def subscribed?
subscription&.active?
end
end
# Usage
current_team.subscribe_to_plan("pro")
Webhook Handlers#
# Jumpstart webhooks (using Pay)
class WebhooksController < Pay::Webhooks::ApplicationController
def handle_subscription_created(event)
# Pay gem handles this automatically
end
end
# Bullet Train webhooks
class Webhooks::Incoming::StripeController < ApplicationController
def create
event = Stripe::Webhook.construct_event(...)
case event.type
when 'customer.subscription.created'
team = Team.find_by(stripe_customer_id: event.data.object.customer)
team.update_subscription_from_stripe(event.data.object)
end
end
end
Step 9: Migrate Background Jobs#
Both use ActiveJob, but patterns may differ:
# Jumpstart Pro job
class SyncAccountDataJob < ApplicationJob
def perform(account)
account.sync_external_data
end
end
# Bullet Train job - same pattern, different naming
class SyncTeamDataJob < ApplicationJob
def perform(team)
team.sync_external_data
end
end
Step 10: Migrate Tests#
Update Factory Definitions#
# Jumpstart factories
FactoryBot.define do
factory :account do
name { Faker::Company.name }
end
factory :account_user do
account
user
owner { false }
end
end
# Bullet Train factories
FactoryBot.define do
factory :team do
name { Faker::Company.name }
end
factory :membership do
team
user
role { 'member' }
end
end
Update Test Helpers#
# Jumpstart test helper
def sign_in_with_account(user, account)
sign_in user
session[:account_id] = account.id
end
# Bullet Train test helper
def sign_in_with_team(user, team)
sign_in user
# Bullet Train stores team context differently
session[:current_team_id] = team.id
end
Common Challenges#
1. Route Structure Differences#
# Jumpstart routes
Rails.application.routes.draw do
scope "/(:account_id)" do
resources :projects
end
end
# Bullet Train routes
Rails.application.routes.draw do
scope "/teams/:team_id" do
resources :projects
end
end
Solution: Update all path helpers:
account_projects_path(account)→team_projects_path(team)edit_account_project_path(account, project)→edit_team_project_path(team, project)
2. API Differences#
# Jumpstart API (often custom)
module Api::V1
class ProjectsController < ApiController
def index
render json: current_account.projects
end
end
end
# Bullet Train API (uses Doorkeeper + jbuilder)
module Api::V1
class ProjectsController < Api::V1::ApplicationController
def index
@projects = current_team.projects
render :index
end
end
end
3. Permission System#
# Jumpstart uses simple owner checks
def authorize_account!
redirect_to root_path unless current_account_user&.owner?
end
# Bullet Train uses CanCanCan with roles
class Ability
include CanCan::Ability
def initialize(user)
if user.admin_of?(current_team)
can :manage, Project, team_id: current_team.id
else
can :read, Project, team_id: current_team.id
end
end
end
Migration Script Template#
# scripts/full_migration.rb
class JumpstartToBulletTrainMigration
def run
ActiveRecord::Base.transaction do
migrate_users
migrate_accounts_to_teams
migrate_memberships
migrate_subscriptions
migrate_custom_models
puts "Migration complete!"
end
rescue => e
puts "Migration failed: #{e.message}"
raise ActiveRecord::Rollback
end
private
def migrate_users
# Users are typically compatible, just copy
puts "Migrating #{User.count} users..."
end
def migrate_accounts_to_teams
Account.find_each do |account|
Team.create!(
id: account.id,
name: account.name,
slug: account.domain || account.name.parameterize,
time_zone: account.time_zone
)
end
puts "Migrated #{Account.count} accounts to teams"
end
def migrate_memberships
AccountUser.find_each do |au|
Membership.create!(
user_id: au.user_id,
team_id: au.account_id,
role: au.owner? ? 'admin' : 'member'
)
end
puts "Migrated #{AccountUser.count} memberships"
end
def migrate_subscriptions
# Map Pay subscriptions to Bullet Train format
Pay::Subscription.find_each do |sub|
# Create corresponding Bullet Train subscription
end
end
def migrate_custom_models
# Your custom models here
end
end
Timeline Estimate#
| Phase | Estimated Time |
|---|---|
| Setup & Planning | 2 hours |
| Database Migration | 4 hours |
| Model Updates | 4 hours |
| Controller Refactoring | 4 hours |
| View Updates | 3 hours |
| Billing Migration | 2 hours |
| Testing | 1 hour |
| Total | ~20 hours |
Conclusion#
Migrating from Jumpstart Pro to Bullet Train is a medium-complexity task centered around renaming (Account→Team) and adopting Bullet Train's conventions. The core Rails patterns are similar, making the migration more about terminology and UI components than fundamental architecture changes.
Bullet Train's Super Scaffolding can help you rebuild features quickly, and the open-source core provides long-term flexibility.
Not sure which boilerplate to choose?
Take our 2-minute quiz and get personalized recommendations.
Take the Quiz