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
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.