Project deep dive
Odessa Symphony Guild
A production membership and payment platform for the Belles & Beaux program — replacing manual spreadsheet intake with structured enrollment, QuickBooks invoicing, and automated payment reconciliation via signed webhooks.
Overview
The Odessa Symphony Guild runs a student membership program called Belles & Beaux. Before this platform, enrollment happened through paper forms and email, dues were tracked in spreadsheets, and payment status required manual reconciliation with QuickBooks. The system worked — until it didn't.
This project replaced that workflow end-to-end: families complete a structured three-step registration with dynamic guardian capture, legal consent, and live dues calculation. Submitting the form generates a QuickBooks customer record and sends a payment-enabled invoice automatically. When payment clears, a signed webhook updates the student's status in Supabase without any admin involvement.
The technical challenge wasn't any single feature — it was the combination: real third-party payment integration, compliance-grade security, schema evolution without breaking historical records, and making sure none of it could fail in a way that blocked a family from registering their kid.
Features
What it does
Technical Highlights
Under the hood
Security
- —AES-256-GCM encryption for QuickBooks OAuth tokens at rest
- —HMAC-SHA256 webhook signature verification with timingSafeEqual (timing-safe comparison)
- —Web Crypto API (crypto.subtle) for Edge-compatible admin session validation
- —httpOnly, sameSite=strict, secure session cookies with 8-hour lifetime
- —All /admin/* routes protected via Next.js middleware with Edge-compatible session verification
- —Secure response headers: no-cache, nosniff, DENY framing on all sensitive API routes
Database
- —Supabase migration set covering students, encrypted QB tokens, settings, guardian expansion, and media-release fields
- —Flexible 1–4 guardian arrays mapped to 36 relational DB columns with zero data loss for existing records
- —Student records scoped by school year for clean multi-year data isolation
- —Backward compatibility: supports both new guardian_N_* columns and legacy mom_*/dad_* records in display
- —Supabase service role key for trusted server-side operations; RLS preserved for public routes
QuickBooks
- —Customer find-or-create flow keyed by primary email to avoid duplicate payer records
- —Invoices with due dates, line items, and ACH/credit-card online payment enabled (AllowOnlinePayment: true)
- —Multi-recipient invoice delivery for additional guardian email addresses
- —intuit_tid correlation IDs logged on all QB API calls for production debugging
- —MOCK_PAYMENT_MODE feature flag with singleton mock QB client — full local dev without live credentials
Frontend
- —React Hook Form + Zod with step-aware validation and progressive completion UX
- —useFieldArray for dynamic guardian management with add/remove and conditional field copying
- —Zod superRefine for cross-field business rules across dynamic arrays
- —Live phone number formatting during keypress for student and guardian inputs
- —Live dues loaded from settings API with resilient hardcoded fallback
Engineering Challenges
Where it got interesting
QB Payment → Invoice reconciliation
QuickBooks webhook events for payments don't include the Invoice ID directly. To automate paid-status sync, you have to traverse Payment.Line[].LinkedTxn[] to find the linked invoice. This structure isn't obvious from the top-level documentation — it required reading the API reference carefully, reproducing the event shape from logs, and building the traversal logic to extract the correct IDs.
Edge runtime crypto rewrite
The original admin session validation used Node.js crypto module, which is unavailable in the Next.js Edge runtime. Middleware runs on the Edge, so all Node APIs break silently or throw. The fix was a full rewrite to Web Crypto API (crypto.subtle) — same algorithm, different API surface, with async/await throughout since crypto.subtle is promise-based unlike the synchronous Node version.
Dynamic guardian schema migration
The original schema had mom_* and dad_* columns — a two-guardian assumption baked into the data model. Expanding to 1–4 guardians required a migration that added 36 new relational columns while keeping all existing records readable and displayable. The display layer had to support both old and new column formats simultaneously without a big-bang migration of historical data.
Graceful QB degradation
QuickBooks is a third-party dependency that can fail at any point — auth expiry, API rate limits, network timeouts. Student registration couldn't be blocked by any of those. The solution was a clear separation: Supabase persistence is the primary operation, QB operations are secondary and non-blocking. Failures are logged with enough context (intuit_tid, error shape) to retry manually if needed.
