Rails 8 for Tour Management: Why We Chose Boring Technology

TourChamp uses Rails 8, SQLite, and Hotwire. Not because it's trendy—because it's reliable, fast, and maintainable by a small team.

Rails 8 for Tour Management: Why We Chose Boring Technology

Rails 8 for Tour Management: Why We Chose Boring Technology

When building TourChamp, we had a choice: follow the hype or use boring, proven technology.

We chose boring. Here's why.

The Problem Domain

TourChamp manages:
- Crew member profiles (photos, contacts, employment history)
- Passport and visa tracking (expiration dates, validity windows)
- Tour schedules (dates, venues, countries)
- Compliance checking (automated credential validation)
- Document uploads (scans of passports, visas, certificates)

Key requirements:
- Fast CRUD operations (creating crew, updating documents)
- Real-time dashboard updates
- Mobile-friendly (tour managers work from venues, airports, buses)
- Simple deployment (we're not Netflix-scale)
- Maintainable by small team (1-2 developers)

The Stack

  • Framework: Rails 8.1
  • Language: Ruby 3.4.2
  • Database: SQLite3
  • Frontend: Tailwind CSS + Hotwire (Turbo + Stimulus)
  • Authentication: Devise
  • Background Jobs: Solid Queue
  • Deployment: Kamal (Docker-based)

No React. No PostgreSQL. No microservices. No Kubernetes.

Why Rails 8?

1. Batteries Included

Rails ships with everything you need:
- ORM (ActiveRecord)
- Authentication scaffolding
- Background jobs (Solid Queue)
- Asset pipeline
- WebSockets (ActionCable)
- Email handling (ActionMailer)

One framework. No decision fatigue.

Compare to Node.js: which ORM? Sequelize? Prisma? TypeORM? Which auth library? Passport? Auth0? Which job queue? Bull? Bee?

Rails answers these questions: use what's included.

2. Convention Over Configuration

Rails conventions mean less code and faster onboarding.

Standard REST routes:
ruby
resources :crew_members do
resources :passports
resources :visas
end

Generates 7 standard routes (index, show, new, create, edit, update, destroy) with conventional controller actions.

New developer joins? They already know the patterns.

3. Solid Queue (No Redis Dependency)

Rails 8 includes Solid Queue: background jobs stored in your database. No separate Redis instance.

Our use case:
- Send expiration reminder emails (90/60/30 days before)
- Generate CSV exports of crew data
- Sync with Google Sheets (for legacy users)

Volume: A few hundred jobs per day. SQLite handles this easily.

Benefits:
- One less service to manage
- Atomic job persistence (same database transaction)
- Simpler infrastructure

When would we switch to Sidekiq/Redis?
- Processing >1000 jobs/minute
- Need Redis for caching anyway
- Real-time analytics requiring fast counters

We're not there. Solid Queue works great.

4. Hotwire (HTML Over The Wire)

Instead of building a JSON API + React frontend, we use Hotwire: server-rendered HTML fragments sent over the wire.

Turbo Frames for partial page updates:
erb
<%# Clicking "Edit" replaces just the crew member card %>
<turbo-frame id="crew_<%= @crew.id %>">
<%= render @crew %>
</turbo-frame>

Turbo Streams for real-time updates:
```ruby

When a credential is updated, broadcast to all viewers

@credential.broadcast_replace_to(
"tour_#{@tour.id}_credentials",
partial: "credentials/credential",
locals: { credential: @credential }
)
```

Stimulus for sprinkles of JavaScript:
javascript
// Auto-save form on blur
export default class extends Controller {
save() {
this.element.requestSubmit()
}
}

Result: Modern SPA-like feel without the complexity of React/Vue/Angular.

5. Simple Deployment (Kamal)

Rails 8 ships with Kamal 2.0: zero-downtime Docker deployments to any VPS.

Deploy process:
```bash

First time setup

kamal setup

Every deploy after

kamal deploy
```

Kamal handles:
- Building Docker images
- Pushing to registry
- Zero-downtime rollout (health checks + graceful swap)
- SSL via Traefik
- Database migrations

Infrastructure:
- Single DigitalOcean droplet ($24/month)
- SQLite database (local file)
- Kamal deployment (5 minutes)

No:
- Heroku ($100+/month)
- Complex CI/CD pipelines
- Kubernetes manifests
- Terraform configs

Deploy early, deploy often. Kamal makes it boring.

Why SQLite (Yes, in Production)

SQLite is fast, reliable, and simple. For single-server apps processing <10,000 requests/hour, it's perfect.

Rails 8 makes SQLite production-ready:
- WAL mode enabled by default (better concurrency)
- Automatic backups via Litestream (stream changes to S3)
- Proper connection pooling

Our decision:
TourChamp serves <100 concurrent users (tour managers, crew members). Requests are simple CRUD operations. No need for PostgreSQL complexity.

When would we switch to Postgres?
- Need horizontal scaling (multiple app servers)
- Advanced SQL features (full-text search, JSON queries at scale)
- Replication requirements

SQLite is faster to start, simpler to backup, and costs $0 extra.

The Data Model

Core entities:

class CrewMember < ApplicationRecord
  has_many :passports
  has_many :visas
  has_many :tour_crew_assignments
  has_many :tours, through: :tour_crew_assignments

  validates :first_name, :last_name, :email, presence: true
  validates :email, uniqueness: true

  # Health status based on credential completeness
  enum documentation_health: {
    critical: 0,    # No valid passport
    important: 1,   # Missing visa for assigned countries
    incomplete: 2,  # Missing optional documents
    complete: 3     # All documentation valid
  }
end
class Passport < ApplicationRecord
  belongs_to :crew_member
  belongs_to :country

  validates :passport_number, :issue_date, :expiry_date, presence: true

  # Check validity with buffer days (most countries require 6 months)
  def valid_for_date?(date, buffer_days: 14)
    expiry_date > date + buffer_days.days
  end
end
class TourLeg < ApplicationRecord
  has_many :tour_leg_shows
  has_many :tour_crew_assignments
  has_many :crew_members, through: :tour_crew_assignments

  # Get all countries visited during this tour
  def countries_visited
    tour_leg_shows.includes(:country).map(&:country).uniq
  end
end

Compliance checking:

class CredentialCheckService
  def check_crew_for_show(crew_member, show)
    results = {}

    # Check passport validity (14-day buffer)
    passport = crew_member.passports.active.first
    results[:passport] = passport&.valid_for_date?(show.date, buffer_days: 14)

    # Check visa requirements
    if show.country.schengen?
      # Schengen zone: check 90/180 day rule
      results[:visa] = check_schengen_validity(crew_member, show)
    else
      # Non-Schengen: check visa for this specific country
      results[:visa] = crew_member.visas.valid_for(show.country, show.date).exists?
    end

    results
  end
end

ActiveRecord makes this readable and maintainable.

Development Workflow

Local development:
```bash

Install dependencies

bundle install

Setup database (creates, migrates, seeds sample data)

bin/rails db:setup

Start server (Puma + Solid Queue + CSS/JS watch)

bin/dev
```

Running tests:
```bash

Full test suite

bin/rails test

Specific test

bin/rails test test/models/crew_member_test.rb
```

Deployment:
```bash

Deploy to production

kamal deploy

Check logs

kamal app logs -f

Rollback if needed

kamal rollback
```

Simple. Boring. Reliable.

What We Didn't Use (And Why)

React / Vue / Angular

Why not: TourChamp is a CRUD app with forms and lists. Server-rendered HTML is fast enough. Hotwire provides SPA-like experience without the complexity.

When we'd use it: If we build a drag-and-drop tour scheduler (visual timeline, complex interactions), we'd reach for React.

PostgreSQL

Why not: SQLite handles our traffic easily. One fewer service to manage.

When we'd switch: If we need horizontal scaling (multiple app servers) or advanced SQL features.

GraphQL

Why not: REST is simpler. We're not building a public API.

When we'd use it: If we had mobile apps (iOS/Android) needing flexible queries.

Microservices

Why not: Complexity for no benefit. We're a small team building a focused app.

When we'd use it: If we had distinct domains (billing, compliance, scheduling) with independent scaling needs. We don't.

Performance

Page load times (production):
- Dashboard: ~150ms
- Crew member profile: ~80ms
- Tour schedule: ~120ms

Database queries:
- Optimized with includes for N+1 prevention
- Indexes on foreign keys and frequently queried columns
- ~5-10ms average query time

Background jobs:
- Email reminders: ~500ms each
- CSV exports: ~2-3 seconds for 100 crew members

Good enough. Users don't notice. We spend time on features, not micro-optimizations.

Lessons Learned

1. Rails is Still Fast

Rails 8 boots in ~2 seconds (development). Zeitwerk autoloading is smart. Hotwire makes the app feel snappy.

2. SQLite is Underrated

Backups are cp database.sqlite3 backup.sqlite3. Migrations are instant. Performance is great for our scale.

3. Simplicity Compounds

One codebase. One database. One deployment. Less complexity = faster iteration.

4. Hotwire is Productive

Building forms, lists, and dashboards is fast. No API contracts, no JSON serialization, no keeping frontend/backend in sync.

When Rails Isn't Right

Don't use Rails if:
- Building a mobile-first app (use React Native or Flutter)
- Need millisecond-level response times (use Go or Rust)
- Team is 100% JavaScript developers (use Node.js)
- Building a single-page design tool (use React)

Use Rails if:
- Building SaaS, e-commerce, internal tools, CRUD apps
- Want to ship fast with small teams
- Value convention and productivity over control

The Bottom Line

TourChamp is built on Rails 8 because:
1. One framework for everything (ORM, jobs, auth, frontend)
2. Fast development (small team, fast iteration)
3. Simple deployment (Kamal + SQLite)
4. Boring technology (proven, stable, maintainable)

We're not trying to be clever. We're trying to solve tour managers' problems.

Rails lets us focus on the domain, not the infrastructure.

If you're building a web application in 2026, Rails 8 is still the best choice for 90% of use cases.

Don't let the JavaScript hype distract you. Boring technology works.


About the Author: Jonny Dalgleish is a Ruby on Rails developer and tour crew member currently touring internationally with Bryan Adams. He builds software to solve real-world problems in touring, logistics, and operations. Contact | GitHub | TourChamp

FREE Shopify Product Migration

Moving to Shopify? We'll migrate your product catalog for free. New stores only.

Learn More