The Problem
Every year, Leisure Travel Vans holds an annual rally attracting 600+ owners from across North America. But capacity is limited to 150 attendees. The challenge: fairly allocate spots, manage invitations, handle cancellations, coordinate check-ins, and communicate with organizers — all without manual intervention.
Before Trip Planner, this was a spreadsheet mess. Manual draws, email chains, cancellation follow-ups, parking assignments done by hand.
The Vision
Start with LTV's internal rally. Once it works, open it as a platform for the 50+ club leaders across the RV community to plan their own events. Dogfood internally first, then expand.
1. Registration & Lottery
Users register via an embedded form in MyLTV. Organizers set lottery parameters (open date, close date, draw date), and the system automatically draws the desired number of invitees. No manual picking.
Technical detail: Lottery is atomic—single database transaction draws, marks invitees, queues notifications. If draw fails mid-way, the whole thing rolls back. No partial draws.
2. Automated Invitations
Each invitee gets a personalized invite with a unique URL. They can:
- Accept or decline
- Update their details (attendee count, dietary restrictions, site preferences)
- Pay for the event
- Download parking assignment
All without leaving MyLTV or email.
3. Self-Healing Cancellations
When someone cancels or declines, the system automatically:
- Moves them to a waitlist if one exists
- Selects the next person from the waitlist
- Sends them an invite
- Updates parking assignments
This runs on Inngest as a scheduled job. The organizer never has to manually fill spots.
4. Email Automation & Templates
Organizers can configure:
- Pre-event reminders (7 days before, 1 day before, 2 hours before)
- Post-event thank you emails
- Custom email templates with event-specific variables
Emails are queued via Inngest and sent at the scheduled time. Organizers see delivery status in the dashboard.
5. Parking Assignments
Events need to space out check-ins. The organizer uploads a CSV parking plan (site numbers, preferred check-in times), and the system:
- Assigns each attendee to a spot
- Respects time-based spacing (e.g., 15 people per 30-min window)
- Balances zone preferences if available
- Regenerates assignments if capacity changes or spots are blocked
Attendees see their assignment in their invitee portal.
Custom Field Configuration
Organizers define what fields registrants fill out:
- Standard: name, email, phone
- Custom: number of attendees, dietary restrictions, site zone preference, RV model
- External: pull data from an external API (e.g., dealer information, previous attendance history)
Fields are validated, sortable, and filterable in the dashboard.
Mailchimp Integration
Automatically create a Mailchimp audience and keep it in sync with registration status:
- New registrants → added to audience
- Accepted invites → tagged "attending"
- Cancelled → tagged "declined"
Organizers can then run targeted campaigns in Mailchimp without manual list management.
Cancellation Policy Tiers
Organizers define refund schedules:
- Cancel 60+ days before: 100% refund
- Cancel 30-60 days: 50% refund
- Cancel < 30 days: no refund
When someone cancels, the system calculates the refund automatically and processes it (or marks it for manual processing if not integrated with payment processor).
Branding & Customization
- Upload event logo
- Customize colors (primary, accent, text)
- Custom event description and terms
- Branded email templates
Attendees see branding throughout their journey (registration, invitee portal, confirmation emails).
How It Works
Registration Phase:
- Organizer creates event (name, date, capacity, fee, lottery params)
- Users register via MyLTV form
- Registrations accumulate until lottery close date
Lottery Phase:
- Draw date arrives → system draws X winners
- Inngest triggers invite email job
- Each winner gets invite with unique URL
Acceptance Phase:
- Attendee clicks unique URL → sees their details form
- They update registration details (attendee count, parking preferences, etc.)
- They pay (if configured)
- System marks them as "confirmed"
Check-In Phase:
- System emails parking assignment
- Attendee shows up at assigned time/location
- Organizer checks them in (or system auto-marks if QR codes are used)
Cancellation Phase:
- Attendee or organizer cancels
- Inngest job marks them as cancelled, calculates refund
- Next waitlist person is automatically invited
- Parking assignments recalculate
- Automated email to cancelled attendee
Post-Event:
- Organizer marks event as complete
- Thank you emails sent automatically
- Feedback surveys sent (optional)
Atomicity & Data Consistency
Lottery draw is a critical operation. If it fails partway, invites go out to the wrong people. The system uses database transactions to ensure either the entire draw succeeds or none of it does.
// Pseudocode
await db.transaction(async (tx) => {
// Lock the registrations table
const registrants = await tx.select()
.from(registrations)
.where(eq(registrations.eventId, eventId))
.forUpdate()
// Shuffle and select
const winners = shuffle(registrants).slice(0, capacity)
// Update all at once
await Promise.all(
winners.map(w => tx.update(registrations)
.set({ status: 'invited' })
.where(eq(registrations.id, w.id))
)
)
// Queue invite emails atomically
await tx.insert(emailQueue).values(
winners.map(w => ({ to: w.email, template: 'invite' }))
)
})Job Scheduling with Inngest
Many operations need to happen at specific times:
- Send reminders 7 days before event
- Auto-fill cancelled spots immediately (but batch process updates)
- Send thank you emails post-event
Inngest handles this as durable, retryable jobs. If a job fails, it retries with exponential backoff. If a server goes down, jobs resume.
Unique Invite URLs
Each invitee gets a cryptographically random URL (e.g., /invite/eW9zaWFoLXVuaXF1ZS1oYXNo). This is a salted, hashed token stored in the database. Prevents URL guessing, allows tracking who clicked their invite.
CSV Parking Plan Upload
Organizers upload a CSV like:
spot_number,preferred_checkin_time,zone,capacity
A1,08:00,North,1
A2,08:00,North,1
B1,08:15,South,1
The system parses it, validates it, and uses it as the source of truth for parking assignments. If spots change, organizer reuploads and assignments recalculate.
Mailchimp Sync
Uses Mailchimp's API to:
- Create audience (if not exists)
- Upsert contacts on every registration change
- Tag based on status (registered, invited, confirmed, cancelled)
Runs as an Inngest job triggered on registration/status changes. If Mailchimp is down, jobs queue and retry.
What Makes This Work
User-first design. Attendees see one URL, one form, one payment. They don't know about the lottery, the parking algorithm, the email queue. It just works.
Operator empowerment. Organizers don't write code. They upload a CSV, configure email templates, set lottery dates. The system handles the complex orchestration.
Resilience. Jobs are durable. If a server crashes mid-email-send, the job retries. If Mailchimp is down, it queues and tries again later. No manual intervention needed.
Flexibility. Custom fields, external data sources, configurable policies. Different events have different needs. The system accommodates without being over-engineered.
What I Learned
Idempotency is everything. If you retry a job (send invite), you need to guarantee sending the same invite twice doesn't create problems. Inngest handles this by storing request IDs and deduplicating.
External API brittleness. Mailchimp sometimes fails. Rather than block the user's workflow, we queue the sync and retry async. The event creates fine; sync catches up later.
Lottery fairness is boring but important. The draw algorithm is trivial (shuffle, slice). The hard part is ensuring it's transparent and auditable. Organizers can see the exact seed used, verify the draw happened correctly.
CSV is underrated. Organizers know CSV. They don't want to use a UI to define 50 parking spots. A CSV upload with validation is faster and less error-prone.
Why This Matters
This started as a side project to solve LTV's own problem. It's evolved into a platform used by 50+ club leaders. It demonstrates:
- Full-stack capability - frontend (React Router, TailwindCSS), backend (Inngest, Supabase), database design
- Production mindset - atomicity, idempotency, error handling, observability
- User empathy - simple for attendees, powerful for organizers
- Shipping velocity - went from idea to production event handling in months, not years
Why This Matters
This started as a side project to solve LTV's own problem. It's evolved into a platform used by 50+ club leaders. It demonstrates:
- Full-stack capability - frontend (React Router, TailwindCSS), backend (Inngest, Supabase), database design
- Production mindset - atomicity, idempotency, error handling, observability
- User empathy - simple for attendees, powerful for organizers
- Shipping velocity - went from idea to production event handling in months, not years
Why This Matters
This started as a side project to solve LTV's own problem. It's evolved into a platform used by 50+ club leaders. It demonstrates:
- Full-stack capability - frontend (React Router, TailwindCSS), backend (Inngest, Supabase), database design
- Production mindset - atomicity, idempotency, error handling, observability
- User empathy - simple for attendees, powerful for organizers
- Shipping velocity - went from idea to production event handling in months, not years