Wildcat Off Road Park: A Custom Booking-and-Payments Platform for a Multi-Accommodation Off-Road Park
Outdoor Recreation & Hospitality (off-road park / campground reservations)
A custom Ruby on Rails reservation-and-payments platform for a Kentucky off-road park that rents cabins, RV sites, and campsites. One booking engine handles both customer self-service and staff-created reservations — date-lock availability that prevents double-booking, Stripe deposits and balances, group bookings for multi-trailer trips, damage charges on a card on file, policy-driven refunds, and an operations reporting hub — live at wildcatoffroadpark.com.
What was the problem?
Wildcat Off Road Park rents a mix of accommodations — cabins, RV sites, and campsites — to people coming to ride. That mix is exactly what off-the-shelf reservation tools handle badly. A generic campground booking app assumes one kind of unit and one way to pay; the park needed a system that could take a customer's self-service booking online and let staff build a reservation for someone on the phone, put both through the same money and availability rules, and never double-book a site. The details are where booking software usually breaks. Availability has to respect same-day turnover, so a guest checking out in the morning and another checking in that afternoon don't collide — but two people trying to hold the same site for the same nights absolutely must. Payments are rarely a single charge: there are deposits, later balances, staff-taken payments, refunds that follow a cancellation policy rather than a flat rule, and damage charged to a card left on file after a guest has gone. Groups of friends show up with three or four trailers and want one trip, one bill, one confirmation — not four separate transactions. None of that is optional at a real park, and none of it comes in a box.
How did we approach it?
We built one booking engine and gave it two front doors. Whether a customer books themselves online or a staff member books on their behalf, the reservation travels the same path: a short-lived date hold (a ReservationIntent), a Stripe payment, and a webhook that turns the intent into a confirmed reservation once the money clears. The system records who created each booking — the guest, or staff acting for them — so attribution is never guessed after the fact. Building both flows on one engine means the availability rules, the pricing math, and the payment handling can't drift apart between the two paths, which is the classic way this kind of software rots. Double-booking is prevented at more than one layer, because a single guard is a single point of failure for the park's core promise. Redis date locks stop two in-flight checkouts from holding the same nights, a partial unique index on active intents enforces it at the database, and the availability comparisons use exclusive date math so legitimate same-day turnover is allowed while true overlaps are rejected. Pricing is calculated from date-range rules and then snapshotted onto the reservation at the moment of booking, so if the park changes a rate next month, past reservations still show what the guest actually agreed to pay. Money is treated as money. Payments run through Stripe with support for deposits, balances, and staff-initiated charges, and refunds are governed by a policy object that is the single source of truth for the refund amount — recording what was actually refunded, partial or full, with an audit trail behind it. Group bookings wrap several reservations into one cart, one charge, and one confirmation, while keeping each site cancellable on its own with a matching per-line Stripe refund. We shipped the riskier pieces — group bookings, the dates-first staff wizard — behind feature flags so they could be rolled out to specific users and demoed before a full release. Around the booking core, we built the operational surface a working park actually runs on: a staff console with a booking calendar, contextual search, and sign-in-as for support; card-on-file damage reporting; a reports hub covering financials, double-bookings, refunds, and monthly tax; first-party analytics with rollups; and the unglamorous reliability layer — audit trails, rate limiting, bot protection, background jobs, and error tracking — that keeps a payments system trustworthy in production.
What did we deliver?
- A unified reservation engine — every booking, customer self-service or
staff-created, flows through the same ReservationIntent path: a short-lived date
hold, a Stripe payment, and a webhook that converts the intent into a confirmed
reservation, with attribution tracking who created it (self vs. staff-for-customer)
- Double-booking prevention — Redis-backed date locking plus a partial unique index
on active intents, with exclusive same-day-turnover date math so a checkout and a
check-in on the same day don't false-conflict
- Cached availability and date-based pricing — a rental availability service
(short-TTL cache keyed by request) and a pricing calculator that applies
date-range rules, with the full price breakdown snapshotted onto the reservation
at booking time so a later rule change never rewrites history
- Multi-payment support — deposits, balances, and staff-initiated charges tracked
per reservation (total paid / balance due / fully-paid), built on the Pay gem over
Stripe
- Group (multi-unit) bookings — a Booking parent wrapping up to five child
reservations into one cart, one charge, and one confirmation, with per-site
independent cancellation and Stripe partial refunds per line item; rolled out
behind Flipper flags for staged release
- Card-on-file damage reporting — a damage-report flow that charges a saved card
through its own payment service, with evidence attachments
- Policy-driven refunds — a refund policy object as the single source of truth for
refund math, recording actual amounts refunded (partial or full) with an audit trail
- An admin operations console — staff booking (including a dates-first reservation
wizard behind a Flipper flag), a month calendar with arrival/departure indicators,
contextual search, and sign-in-as (masquerade) for support
- A system reports hub — financial, double-booking, and refund reports plus
monthly tax reporting, some scheduled and emailed to admins on a recurring job
- First-party analytics — a PageView pipeline with daily rollups and retention
pruning, so traffic reporting doesn't depend on a third-party tag
- Lead capture for non-bookable inventory — an inquiry form (honeypot + Cloudflare
Turnstile + rate limiting) that routes qualified leads to the owner by email
- Reliability and safety engineering — Solid Queue background jobs, PaperTrail audit
trails, Rack::Attack rate limiting, Cloudflare Turnstile bot protection, Honeybadger
error tracking, and Postmark transactional email
What was the outcome?
Wildcat Off Road Park runs its real reservations and payments on this platform today at wildcatoffroadpark.com. Guests book cabins, RV sites, and campsites online; staff build bookings for callers through the same engine; deposits and balances are collected, refunds follow the park's policy, and damage is charged to a card on file — all in one system built for how this park actually operates rather than bent to fit a generic tool. It's an ongoing engagement: features like group bookings and the staff booking wizard ship behind flags and roll out deliberately.
Results
- hosting
- Hatchbox on DigitalOcean (Kamal migration in progress)
- live_url
- https://wildcatoffroadpark.com
- payments
- Stripe via the Pay gem — deposits, balances, staff charges, card-on-file damage charges, policy-driven partial/full refunds
- framework
- Ruby on Rails 8.1 on Ruby 3.4
- reliability
- PaperTrail audit trails, Rack::Attack rate limiting, Cloudflare Turnstile, Honeybadger error tracking
- booking_model
- Single ReservationIntent path for both customer and staff bookings; webhook-confirmed
- group_bookings
- Booking parent over up to 5 child reservations; per-site cancellation; per-line Stripe refunds; Flipper-gated rollout
- background_jobs
- Solid Queue (PostgreSQL-backed)
- engagement_type
- Client engagement (custom platform build + ongoing product work)
- double_booking_prevention
- Redis date locks + partial unique index on active intents + exclusive same-day-turnover date math
Tech stack
- Ruby on Rails 8.1
- Ruby 3.4
- PostgreSQL
- Hotwire (Turbo + Stimulus)
- Tailwind CSS
- esbuild
- Propshaft
- Solid Queue
- Redis
- Action Cable
- Stripe (Pay gem)
- Devise
- Pundit
- PaperTrail
- Flipper
- Postmark
- Honeybadger
- Cloudflare (DNS / CDN / Turnstile)
- Hatchbox (DigitalOcean)
Want results like these?
Tell us about your project and we'll walk through how we'd approach it.