Migrating from Jumpstart Pro to Bullet Train
medium
20 hours
rails
ruby
open-source
scaffolding

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#

AspectJumpstart ProBullet Train
LicensePaid subscriptionOpen-source + Pro add-ons
UI FrameworkTailwind + customTailwind + custom theme
JS FrameworkHotwire/TurboHotwire/Turbo
Multi-tenancyAccountsTeams
ScaffoldingStandard RailsSuper Scaffolding
BillingPay gemPro 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 ProBullet TrainNotes
AccountTeamMain organizational unit
AccountUserMembershipUser's role in organization
account_idteam_idForeign key reference
current_accountcurrent_teamHelper 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 ComponentBullet Train Component
render "form_field"<%= render 'shared/fields/...' %>
Modal partialsBullet Train modals
Flash messagesBullet 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#

PhaseEstimated Time
Setup & Planning2 hours
Database Migration4 hours
Model Updates4 hours
Controller Refactoring4 hours
View Updates3 hours
Billing Migration2 hours
Testing1 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.

#rails#ruby#open-source#scaffolding

Not sure which boilerplate to choose?

Take our 2-minute quiz and get personalized recommendations.

Take the Quiz
Migrating from Jumpstart Pro to Bullet Train | MyStarterStack