Back to all posts

Build a Complete CRM in 2 Hours: Rails 8 Rapid Prototyping (Part 1)

May 19, 2025
Robin Goudeketting
9 min read
mvp developmentrapid prototypingcrmweb developmentruby on railsrails 8 tutorialstartup developmentminimum viable productweb developmentstartup mvp development
Build a Complete CRM in 2 Hours: Rails 8 Rapid Prototyping (Part 1)

TL;DR

In this article, I demonstrate the power of rapid prototyping with Ruby on Rails by building a fully functional company management MVP in under two hours. This article (part one) covers project setup with PostgreSQL and Bootstrap, creating an Industry model with automatic data seeding, and implementing a complete Company model with industry associations. By the end of this first installment, we have a working CRM interface with automatic slug generation and professional Bootstrap styling. In part two, we will add user authentication and company-user relationships.

Introduction

In business, the gap between idea and implementation often determines success. As I discussed in my article on why Rails remains the ultimate framework for rapid prototyping in 2025, the ability to quickly transform concepts into working software remains invaluable for startups and established businesses alike.

Rather than just making claims, I've decided to show you exactly what's possible. My challenge: build a complete MVP for a company management system—essentially a minimal CRM—in just 2 hours.

Why this matters for your business: When stakeholders can interact with a functional prototype that looks professional, even with minimal effort, buy-in happens faster. This is where Rails 8's built-in tools and conventions1 deliver real business value—letting us focus on solving problems rather than wrestling with configuration and boilerplate code.

In this series, we'll build a complete company management system featuring:

  • Secure user authentication
  • Industry categorization for companies
  • Role-based user-company relationships
  • A responsive Bootstrap interface
  • A functional dashboard with key metrics

Let's set the timer and see what we can accomplish in our first session.

Planning our MVP

⏱️ Elapsed time: 0:00Time remaining: 120:00

Every successful project begins with clear planning. For our CRM MVP, we need:

  • Companies: The core data we'll track
  • Industries: For categorizing our client companies
  • Authentication: So users can manage their own private client lists

We'll build using the latest Ruby on Rails 8, with Git for version control. This foundation will allow us to validate our concept with real users while maintaining the flexibility to iterate quickly.

Setting up the foundation

⏱️ Elapsed time: 4:32Time remaining: 115:28

Let's start by creating our Rails project:

rails new company_crm --database=postgresql --css=bootstrap
>_ bash

This single command builds up our entire application skeleton with PostgreSQL database integration and Bootstrap styling. After waiting about 90 seconds for the setup to complete, we'll have a project structure like this:

Project outline after running the rails new command

One of the many advantages of Rails is that it configures Git automatically. The only remaining task is to set up our local database:

rails db:create
>_ bash

Now we can launch our application with bin/dev and visit http://localhost:3000:

Our new app first loading screen

Let's commit our initial framework:

git add .
git commit -m 'Initial commit :tada:'
>_ bash

Building core models

⏱️ Elapsed time: 6:33Time remaining: 113:27

Next, we'll create our fundamental data models. We need two key components:

  1. Industries: Categories for our companies
  2. Companies: The main entities in our CRM

For rapid prototyping, we'll start with a pre-defined list of common industries rather than building management features for them. This approach lets us focus on core functionality first and refine later.

rails generate model Industry name:string slug:string
>_ bash

This command creates our Industry model and database migration. To populate our industries, we'll create a seed file. First, let's set up our seeds structure:

# db/seeds.rb
puts "Running seeds..."
 
Dir[Rails.root.join('db', 'seeds', '*.rb')].sort.each do |file|
  puts "Loading seed file: #{file}"
  require file
end
 
puts "Seeds completed!"
>_ ruby

This organization allows us to maintain separate seed files for each resource type. Now let's create db/seeds/industries.rb:

['Education', 'Entertainment', 'Finance', 'Healthcare', 'Technology', 'Agriculture',
'Construction', 'Manufacturing', 'Telecommunication', 'Hospitality', 'Mass-media',
'Mining', 'Retail', 'Transportation', 'Aerospace', 'Energy', 'Law', 'Pharmaceutical',
'Food'].each do |industry|
  Industry.find_or_create_by!(name: industry, slug: industry.parameterize)
end
>_ ruby

Let's run our migration and seed the database:

rails db:migrate
rails db:seed
>_ bash

Now for the main event—our Company model. Since we need full CRUD (Create, Read, Update, Delete) functionality, we'll use Rails' scaffold generator2 to save significant development time:

rails generate scaffold Company name:string slug:string industry:references description:text last_contacted_at:datetime
rails db:migrate
>_ bash

With just these commands, we've created:

  • A complete data model
  • Database migrations
  • A controller with all CRUD actions
  • Views for listing, showing, creating, and editing companies
  • API endpoints for all operations
  • Tests for our new functionality

Visiting http://localhost:3000/companies shows our new company interface:

Companies page

Let's enhance our Company model with automatic slug generation. The following method will only recreate the slug if the name changed. This will keep the slug in line with the company name.

# app/models/company.rb
class Company < ApplicationRecord
  # ...
  before_save :update_slug
 
  def update_slug
    self.slug = name.parameterize if name_changed?
  end
end
>_ ruby

And establish the relationship from Industry to Company:

# app/models/industry.rb
class Industry
  has_many :companies
end
>_ ruby

Now we'll update our form to remove manual slug input (since it's automatic) and add industry selection:

<!-- Remove this section from app/views/companies/_form.html.erb -->
<div>
  <%= form.label :slug, style: "display: block" %>
  <%= form.text_field :slug %>
</div>
 
<!-- Replace the industry field with this -->
<div>
  <%= form.label :industry_id, style: "display: block" %>
  <%= form.select :industry_id, Industry.pluck(:name, :id) %>
</div>
>_ erb

With these changes, we can now create companies with proper industry relationships and automatic slug generation.

Adding professional styling

⏱️ Elapsed time: 17:23Time remaining: 102:37

Our functionality is solid, but the interface needs polish. Let's leverage Bootstrap3 to create a professional look with minimal effort.

First, we'll add the Bootstrap CDN links to views/layouts/application.html.erb:

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js" integrity="sha384-j1CDi7MgGQ12Z7Qab0qlWQ/Qqz24Gc6BM0thvEMVjHnfYGF0rmFCozFSxQBxwHKO" crossorigin="anonymous"></script>
>_ html

Then, there are some changes we need to make in the css build scripts. By default, it generates the scripts with bun for me, even though it's using yarn for everything else. In your package.json update the following lines:

{
  // ... other settings
  "scripts": {
    // ... other scripts
    "build:css": "yarn run build:css:compile && yarn run build:css:prefix",
    "watch:css": "nodemon --watch ./app/assets/stylesheets/ --ext scss --exec \"yarn run build:css\""
  }
}
>_ json

This tells nodemon to run everything with yarn instead of bun. Next, let's transform our company index view into a clean, professional table:

<%# views/companies/index.html.erb %>
<p style="color: green"><%= notice %></p>
<% content_for :title, "Companies" %>
<div class='container'>
  <h1>Companies</h1>
  <table id="companies" class='table table-hover'>
    <thead>
      <th>Name</th>
      <th>Industry</th>
      <th>Last contacted at</th>
      <th></th>
    </thead>
    <tbody>
      <% @companies.each do |company| %>
        <%= render company %>
      <% end %>
    </tbody>
  </div>
  <%= link_to "New company", new_company_path, class: 'btn btn-outline-primary' %>
</div>
>_ erb

And update our company row partial:

<%# views/companies/_company_.html.erb %>
<tr id="<%= dom_id company %>">
  <td><%= company.name %></td>
  <td><%= company.industry.name %></td>
  <td><%= company.last_contacted_at %></td>
  <td>
    <%= link_to 'Show', company_path(company), class: 'px-2' %>
    <%= link_to "Edit", edit_company_path(company) %>
  </td>
</tr>
>_ erb

For our form elements, we'll add Bootstrap classes to enhance the visual experience:

  • Add form-label class to each form.label
  • Add form-control class to each input field

Here's the updated edit page with proper card styling:

<% content_for :title, "Editing company" %>
<%= link_to "‹ Back to companies", companies_path, class: '' %>
<div class='card my-4'>
  <h1 class='card-header'>Editing <%= @company.name %></h1>
  <%= render "form", company: @company %>
</div>
<div>
  <%= link_to "Cancel", @company, class: 'btn btn-outline-danger' %>
</div>
>_ erb

Finally, let's create a visually appealing company detail page:

<section id="<%= dom_id company %>" class='card my-4'>
  <h1 class='card-header'><%= company.name %></h1>
  <div class='card-body'>
    <p class='card-text'><%= company.description %></p>
    <p class='card-text'>
      <strong>Industry:</strong>
      <%= company.industry.name %>
    </p>
    <% if company.last_contacted_at.blank? %>
      <p class='card-text'>You haven't contacted <em><%= company.name %></em> yet.</p>
    <% else %>
      <p class='card-text'>
        <strong>Last contacted at:</strong>
        <%= company.last_contacted_at %>
      </p>
    <% end %>
  </div>
</section>
>_ erb

End of part one

⏱️ Elapsed time: 36:50Time remaining: 83:10

In just over 30 minutes, we've established a solid foundation for our CRM MVP. Here's what we've accomplished:

  1. Set up a complete Rails 8 application with PostgreSQL and Bootstrap
  2. Created and populated an Industry model for classification
  3. Built a full-featured Company model with industry relationships
  4. Implemented automatic slug generation for SEO-friendly URLs
  5. Created a professional-looking interface using Bootstrap styling

Here's our current application in action:

The company index pageThe company show pageThe company edit page

In Part 2, we'll add user authentication, link companies to specific users, and create a personalized dashboard. Depending on how much time there is left, this could complete our minimum viable product, demonstrating how Rails enables businesses to validate ideas with working software in record time. However, if there's more time, we might get to add some nice-to-haves; filtering and pagination.

If you're looking to rapidly validate your next business concept with a functional prototype, let's talk. Rapid prototyping might be exactly what your next project needs to gain traction and stakeholder buy-in.

Footnotes

  1. Rails has extensive documentation with guides that can help you get started quickly. https://guides.rubyonrails.org/

  2. The generators that are built into the Rails framework are really powerful when rapid prototyping. https://guides.rubyonrails.org/generators.html

  3. Bootstrap is a tried and true utility-based styling framework. https://getbootstrap.com/docs/5.3/getting-started/introduction/