Building City of Om: A Festival Booking System That Handles 4,000 Yogis
How we built Ottawa's largest wellness festival platform -- Eventbrite sync, slot-based booking, bilingual content, and AI-cleaned bios. All on Rails 8 with SQLite.
Building City of Om: A Festival Booking System That Handles 4,000 Yogis
City of Om is Ottawa's largest wellness festival. Three days at Lansdowne Park, dozens of classes, hundreds of teachers and vendors, and thousands of attendees who all want to book their favourite yoga class before it fills up.
We built the platform that makes it work.
The Problem
Festival logistics are deceptively complex. You've got attendees who bought tickets on Eventbrite. Teachers who applied through Google Forms. Vendors who need booth assignments. Classes that have capacity limits. And everything needs to work in English and French because this is Ottawa.
Before we got involved, the organizers were managing this with spreadsheets, email threads, and a lot of hope. The booking process was "show up and see if there's space." For a festival expecting 4,000+ people across 3 days, that doesn't scale.
Eventbrite as the Source of Truth
Tickets are sold on Eventbrite. We didn't fight this -- Eventbrite handles payment processing, refunds, and ticket distribution better than anything we'd build ourselves. Instead, we made our system sync with Eventbrite in real-time.
The Eventbrite::AttendeesImporter runs every 5 minutes via cron, pulling new ticket purchases and creating User accounts automatically. Each attendee gets a password-less account linked to their ticket email. When someone buys a ticket at 2pm, they can log in and start booking classes by 2:05.
We also set up webhook endpoints for real-time updates. When someone checks in at the gate, cancels their ticket, or gets a refund -- our system knows immediately. No stale data, no manual reconciliation.
The same sync runs for the class catalog. Teachers and events are managed in our admin but cross-referenced with Eventbrite event IDs so everything stays in lockstep.
The Booking Slot System
This is where it gets interesting. Every ticket comes with 4 bookable class slots. Not unlimited, not pay-per-class -- exactly 4. This creates a constraint that changes how people interact with the system.
When someone logs in, they see the full festival schedule. They can bookmark classes they're interested in, browse teacher profiles, and filter by modality (Yoga, Meditation, Dance, Strength, Recovery). But they can only book 4.
The booking logic validates multiple constraints simultaneously:
- Slot availability: Has this user already used their 4 slots?
- Event capacity: Is the class full? (Each AgendaLocation has a capacity with an optional buffer percentage)
- Ticket type: VIP tickets get early booking access. General admission opens later.
- Time conflicts: We flag (but don't block) overlapping bookings at the same time slot.
- Booking windows: Settings control when VIP and general bookings open and close.
The 4-slot limit is calculated from the Attendee records matching the user's email. If someone bought 2 tickets, they get 8 slots. Family purchases stack correctly.
The Day Planner
Once people book their classes, they need to see their schedule. The Day Planner shows a timeline view of booked classes by day, with location details and teacher info.
The killer feature: shareable schedules. Each user gets a unique share_key, and anyone with the link can view (but not modify) someone's plan. Groups of friends going to the festival together share their planners to coordinate. "I'm in the 10am yoga -- want to join?"
We also built iCalendar export so people can add their festival schedule directly to Google Calendar or Apple Calendar.
Teacher and Vendor Applications
Teachers don't just show up. They apply months in advance, and the review process is substantial.
TeacherApplications track everything: proposed class titles, descriptions, experience level, equipment needs, whether they want live music accompaniment, and what modalities they teach. The application workflow moves through pending, reviewed, accepted, rejected, or waitlisted states.
Applications come in through multiple channels:
- Direct form submissions on the website
- Google Sheets imports (the organizers had an existing spreadsheet workflow we didn't want to disrupt)
- Eventbrite event data for returning teachers
The Google Sheets integration polls every 10 minutes via SheetsImportJob, syncing new rows and updates. Teachers who applied via a Google Form get automatically imported without anyone copying and pasting between systems.
VendorApplications follow the same pattern but track different details: booth size preferences, electricity needs, product types, and business descriptions.
AI-Cleaned Content
Here's where it gets fun. Teacher bios and class descriptions come in... rough. Some are beautifully written. Others are a wall of text with no paragraph breaks, excessive exclamation marks, or written in third person when first person would be better.
We integrated Claude (Sonnet) to clean up application content automatically. The AiContent::ClaudeClient generates suggestions for improved bios and descriptions. These aren't auto-applied -- they're presented as suggestions that admins can review and accept.
The pipeline runs: application submitted -> AI generates cleaned version -> admin reviews -> accepted content gets translated to French.
This saved the organizers dozens of hours of manual editing. Instead of rewriting 80 teacher bios, they review AI suggestions and click approve.
Bilingual Everything
Ottawa is officially bilingual. The festival serves English and French speakers. Every piece of public-facing content needs to exist in both languages.
We use the Mobility gem for translatable model attributes. AgendaEvents, AgendaHosts, Pages, Tickets -- all have English and French versions of their text fields. The DeepL API handles automatic translation via an async Translation::DeeplService.
The workflow: content is created in English, AI-cleaned if it's an application, then automatically translated to French via DeepL. Admins can review and adjust the French translation if the machine version misses nuance.
Routes are locale-aware via route_translator: /en/schedule and /fr/horaire show the same content in different languages. The language switcher preserves your current page and context.
The Admin Dashboard
Festival organizers aren't developers. The admin panel needs to be comprehensive but not overwhelming.
Key admin features:
- CSV import/export for everything -- teachers, events, vendors, FAQs, team members
- Eventbrite sync controls -- trigger manual imports, view sync status, handle webhook failures
- Application review workflow -- bulk accept/reject, AI suggestion review, conversion to confirmed hosts/vendors
- Settings panel -- toggle booking windows (VIP vs general), select themes, manage feature flags
- Magic link invites -- new admin team members get a secure invite link via AdminInviteMailer
- Login-as -- admin impersonation for debugging user issues without asking for passwords
The dashboard shows real-time stats: new applications, recent attendees, booking counts, and capacity utilization across locations.
Spatial Planning (The Experimental Bits)
Festival logistics aren't just digital. Vendors need physical booth assignments. Events happen at specific locations on the Lansdowne grounds.
We built beta features for spatial planning:
Vendor Mapper -- An interactive map where organizers drag-and-drop vendors onto tent placements. Each TentPlacement has geo-bounds (northeast/southwest lat/lng coordinates). VendorTypes have color codes so organizers can see at a glance whether food vendors are clustered together or spread out.
Location Planner -- Visualize event locations on the festival grounds with capacity planning. See which spaces are overbooked and which have room.
Map Editor -- Draw custom annotations on the festival map: rectangles, polygons, circles, text labels, and icons. Staff-only layers for backstage areas and public layers for attendee wayfinding.
These features are marked as beta/experimental, but they solve a real problem: the festival team was using printed maps and sticky notes for vendor placement. Now it's interactive and shareable.
The Ticket Pricing Engine
Ticket pricing isn't static. City of Om uses a multi-period pricing model:
coming_soon -> early_bird -> between_periods -> regular -> finished
Each period has different prices and CTAs. Early bird gets a discount. The "coming soon" period shows a notify-me button instead of a buy button. The system automatically transitions between periods based on configured dates.
VIP and standard tickets have different feature lists (drag-and-drop sortable in admin), and add-on tickets let attendees purchase extras like meal packages.
The Stack
- Rails 8.0.1 -- Latest Rails with all the modern defaults
- SQLite -- Production database. The festival runs for 3 days. SQLite handles the load effortlessly.
- Hotwire (Turbo + Stimulus) -- Interactive booking UI without a JavaScript framework
- Tailwind CSS -- Consistent styling across the public site and admin
- Solid Queue -- Background jobs for imports, translations, AI processing, and email
- Whenever gem -- Cron scheduling for periodic Eventbrite syncs and admin reports
No Devise. We rolled custom authentication with has_secure_password, session management, magic link password resets, and rate limiting (10 login attempts per 3 minutes). For a festival app where most users authenticate once and stay logged in for the weekend, this is simpler than configuring Devise's 47 modules.
What Worked
Eventbrite sync was the right call. Fighting the existing ticket platform would have been a waste. Syncing with it gave us the best of both worlds: Eventbrite's payment infrastructure and our custom booking logic.
The 4-slot limit created urgency. People carefully chose their classes instead of booking everything and not showing up. No-show rates were dramatically lower than previous years.
AI content cleanup saved weeks of work. 80+ teacher bios cleaned and translated in hours instead of weeks.
SQLite in production was fine. The festival peaked at maybe 500 concurrent users during booking windows. SQLite didn't flinch.
What We'd Improve
The Google Sheets sync is fragile. Polling every 10 minutes works, but if someone restructures the spreadsheet columns, the import breaks silently. We'd add better validation and error reporting.
The spatial planning tools need more polish. They work for the organizers who built them, but new team members find the map editor unintuitive. Better onboarding or a simpler drag-and-drop interface would help.
Email notifications could be richer. Right now, attendees get basic booking confirmations. We'd add class reminders, teacher change notifications, and "your bookmarked class is almost full" alerts.
City of Om is Ottawa's largest wellness festival. The platform is built and maintained by Loadout. Need a festival or event management system? Get in touch.