← All Projects
American Youth Dance Theater2026

Custom-builtenrollment system.

Frontend, backend, pricing engine, and payments.

~80%
Net Profit Saved
~$13K/yr in annual savings
1,000+
Active Users
During peak registration
5
Third-Party Integrations
Payments, SMS, email, error tracking, caching
3-Layer
Security Model
RLS, middleware, server-action guards
65
Tables in Production
10 domain clusters
Client Portal
aydance.org
AYDT enrollment portal
Admin Portal
admin.aydance.org
AYDT admin dashboard
Stack
Next.jsTypeScriptSupabaseTailwind
Integrations
Twilio SMSResendElavonSentryUpstash Redis
Overview

Four engineering problems defined this build.

01
Getting the Domain Model Right
5 schema migrations in days. Pricing rules and enrollment constraints co-evolved faster than anticipated.
Context

The client's domain understanding was still evolving during the build. Pricing rules, enrollment constraints, and fee configuration all shifted within a compressed window. Every migration had to leave existing data valid.

Migration Trail
hybrid_pricing_model
Added progressive discount bands alongside fixed-cost programs. Required updating the computation engine to handle both paths.
per_day_enrollment
Shifted the registrable unit from sessions to classes with per-day scheduling. Required updating time-conflict trigger logic.
fix_discount_rule_sessions_fk_conflict
First attempt at linking discount rules to sessions created an ambiguous FK path. See callout below.
tuition_engine
Rewired the pricing computation to read from the new hybrid model. Can't add discount bands without also updating the engine.
extended_fee_config
Added conditional fee rules (costume per-class, video by level, registration exemptions). Depended on the corrected discount FK structure.
FK CONFLICT — THE TWO-STEP FIX
The first migration linking discount rules to sessions created an ambiguous foreign key path — two different routes from discount rules to the same sessions table. Postgres accepted it, but queries became unpredictable depending on which path the planner chose.
A follow-up migration (fix_discount_rule_sessions_duplicate_fk) resolved the ambiguity by collapsing to a single FK path. In a live system, the first migration would have meant silent data corruption. In a migration-driven schema, it was a recoverable two-step correction with a full audit trail.
The lesson: The cost of being wrong is determined by your tooling, not your planning. RLS policies continued enforcing data boundaries through every migration. The schema evolved; the security guarantees didn't.
5
Schema Migrations
Within days of each other
0
Data Integrity Breaks
Every migration left existing data valid
65
Tables in Production
RLS enforced through every change

Once the schema was stable, the pricing engine could be built.

02
A Complex Pricing Engine
Server-side pricing for 1,000+ families. Three calculation paths, no room for error.
Context

Before checkout completes, every item in the cart runs through a multi-step pricing pipeline:

Calculated per student, across three possible pricing paths
Combined at the family level with additional discounts and fees
Runs entirely server-side
Step 1 · Per-student calculation
Path 1
Tiered Rate Table
By level and days per week. More days = lower per-class cost.
Path 2
Set Price for the Term
Flat cost for specialty programs. Independent of rate tables.
Path 3
Admin Override
Manual dollar amount. Bypasses all rate tables.
One student can hit multiple paths in the same checkout
Student Tuition Resolved
+
Fees
Costume · Video · Registration
exemptions
Session Discounts
% first, then $ on reduced amount
order matters
=
Student Subtotal
↻ Repeat for next student in cart
Step 2 · Family calculation
Σ
Sum all student subtotals
Family Discount
$50 if 2+ students, once per term
cross-txn
+
Auto-Pay Admin Fee
$5/month × installments, if auto-pay
Coupon
Applied last, validates caps + stackability
gated
=
Grand Total
→ PAYMENT SCHEDULE
Example
See the pricing engine in action
A two-student, multi-path checkout walked through step by step.
Scenario
Sofia, 15 — Senior Division
3 standard classes: Ballet (Tue), Contemporary (Wed), Jazz (Thu)
1 special program: Technique
Enrollment: Full semester
Mateo, 9 — Junior Division
2 standard classes: Hip-Hop (Mon), Tap (Wed)
Enrollment: Full semester
Engine evaluation — step by step
Sofia
Senior
Tuition — tiered rate tableSenior, 3 distinct days — weekly_class_count = 3
$2,269.83
Why distinct days, not class count? The engine counts unique days of the week across all enrolled schedules. If Sofia took two classes on the same Tuesday, that's still weekly_class_count = 1 for pricing purposes. This prevents a student from being penalized with a higher rate tier for schedule density on a single day.
Progressive pricing is real. 3× the single-class Senior rate would be 3 × $796.43 = $2,389.29. The tiered rate for weekly_class_count = 3 charges $2,269.83, a built-in $119.46 savings. The more classes per week, the lower the effective per-class cost.
Technique — fixed-cost programSet price for the term. Bypasses rate tables and discounts entirely.
+$716.78
Two code paths, one student. Sofia's standard classes hit the tiered rate table path. Her Technique enrollment hits the fixed-cost program path. Both fire within the same per-student loop.
Costume fee$65/class × 3 standard classes · Technique is exempt
+$195.00
Recital video fee$15 flat · Senior only
+$15.00
Registration fee$40/student · not discountable · not waived (has standard classes)
+$40.00
Sofia subtotal
$3,236.61
Mateo
Junior
Tuition — tiered rate tableJunior, 2 distinct days — weekly_class_count = 2
$1,513.06
Same progressive model. 2× the Junior single-class rate would be 2 × $775.93 = $1,551.86. The tiered rate charges $1,513.06, saving $38.80.
Costume fee$55/class × 2 standard classes
+$110.00
Recital video feeJunior division — not applicable
Registration fee$40/student · not discountable
+$40.00
Mateo subtotal
$1,663.06
Family-level aggregation
Combined dancer subtotals$3,236.61 + $1,663.06
$4,899.67
Family discount2+ dancers enrolled · first confirmed registration this semester
−$50.00
Cross-transaction eligibility check. The family discount isn't just “2+ dancers in cart.” Before applying, the engine queries all prior confirmed registration batches for this family and semester. If any prior batch already received the discount, it's not awarded again. This is enforced at the database level, not the cart level.
Coupon / promo codeNone applied · validated last, after all other discounts
Cart total — server-validated
$4,849.67
Code · Demo
Annotated code snippet + screen recording
The convergence point where rate tables, fees, and discounts resolve into a cart total. 60–90 second screen capture in demo environment.

A deterministic pricing engine is only useful if you can actually collect the money.

03
Secure Payments
Real payments, concurrent webhooks, children's data. A four-layer flow with security at every checkpoint.
Context

This isn't a Stripe integration. The payment processor is Elavon's EPG, a bank-grade gateway used by enterprise and institutional clients. No SDK. HTTP Basic auth with a merchant alias. HATEOASHypermedia as the Engine of Application StateA REST pattern where API responses include links to related resources. The client navigates by following those links rather than constructing URLs. Each step returns the href needed for the next.-style resource references.

Why this matters
No SDK. Every API call is a raw HTTP request.
Stored payment methods are live href pointers into Elavon's system.
Strict creation order: shopper → stored method → transaction. Partial failures require rollback.
Payment flow

The payment flow is four layers deep. Each layer has a security checkpoint.

Layer 1
API Client
Layer 2
Session Creation
Layer 3
Webhook Handler
Layer 4
Auto-Charge
Layer 1
API Client
CREDENTIALS NEVER LEAVE SERVER
Server-only. Auth built fresh per-request from env vars. Structured error parsing for processor-specific failure codes.
Layer 2
Session Creation
SERVER-SIDE AMOUNT VALIDATION
Server re-reads the batch from the database and rejects any mismatch > $0.01. Client-submitted total is never trusted. Existing pending sessions are reused, not duplicated.

Key decision: doCapture: false on installments. Separates card collection from charging.
Layer 3
Webhook Handler
FOUR SECURITY CHECKS BEFORE BUSINESS LOGIC
Timing-safe auth. crypto.timingSafeEqual prevents credential brute-forcing via response time.
Never trusts the payload. Re-fetches full transaction from processor's API with server credentials.
Replay protection. Terminal-state webhooks acknowledged with 200, zero side effects.
HTTP 201 ≠ approval. Processor returns 201 on declines. Handler checks isAuthorized, not status code.
See the webhook handler code
Timing-safe auth, payload re-fetch, and replay protection from route.ts
app/api/webhooks/epg/route.ts
// Node.js runtime enforced — edge lacks crypto.timingSafeEqual
export const runtime = "nodejs";

// Timing-safe auth comparison
const expectedBuf = Buffer.from(expected);
const actualBuf = Buffer.from(
  authHeader.padEnd(expected.length, "\0").slice(0, expected.length),
);
authValid =
  expectedBuf.length === Buffer.from(authHeader).length &&
  crypto.timingSafeEqual(expectedBuf, actualBuf);

// Never trust the notification body — always re-fetch
let transaction;
try {
  transaction = await fetchEpgTransaction(resource);
} catch (err) {
  return NextResponse.json(
    { error: "Failed to fetch transaction" },
    { status: 500 }
  );
}

// Terminal state replay protection
const terminalStates = [
  "authorized", "captured", "settled",
  "declined", "voided", "refunded"
];
if (terminalStates.includes(payment.state)) {
  return NextResponse.json({ ok: true });
}
Webhook as Sole Writer
The browser redirect is untrusted. No registration can be confirmed without an authorized payment event from the processor.
Layer 4
Auto-Charge
3-STRIKE CAP · ADMIN ESCALATION
Deno edge function finds overdue installments, attempts auto-charge, sends admin summaries. After 3 failed attempts, escalates to admin review. SMS notifications to opted-in parents via Twilio.
42
Unit tests
100%
Payment path coverage
0
Duplicate confirmations
Three-layer data security model
Defense-in-depth for children's enrollment data. Three independent enforcement points.
L1
DATABASE
Postgres Row-Level Security
Parents only query their own family's rows. Admins only access their studio's data.
catches bad queries
L2
MIDDLEWARE
Next.js Navigation Guards
Every route transition verifies role and session before the page renders.
catches URL manipulation
L3
ACTION
Server-Action Guards
Every sensitive mutation re-verifies identity and permissions before executing.
catches privilege escalation
Why not just one? Middleware alone doesn't protect against direct API calls. RLS alone doesn't prevent unauthorized navigation. Server-action guards alone don't stop data leakage through read paths.

Pricing and payments solved, the hardest concurrency problem remained.

04
A Multi-Step Registration Flow
Concurrent enrollment without race conditions. Seat integrity lives in the database.
Context

Registration is a multi-step flow. A family shouldn't lose their spot while filling out a payment form. But you can't reserve spots indefinitely — abandoned carts would lock out real registrations. And two families can't both hold the last spot in a class.

Four-Layer Seat Protection
Layer 1
Timed Holds
15-MINUTE CHECKOUT WINDOW
pending_payment status and a hold_expires_at timestamp written at checkout start. The spot is theirs while they're in the flow.
Layer 2
Overlap Trigger
POSTGRES TRIGGER ON EVERY INSERT
Checks for schedule overlap (same day, overlapping time) against confirmed registrations and non-expired holds. If it only checked confirmed rows, two families could both get holds on the same spot simultaneously.
Layer 3
Hold Expiry
PG_CRON EVERY 5 MINUTES
Expires stale holds and cascade-fails their parent batches atomically. Abandoned checkouts free their spots within 5 minutes.
Layer 4
Waitlist
AUTO-PROMOTE + NOTIFY
Detects newly freed spots, promotes the next waitlist entry, sets an invitation window, and notifies the family via email and SMS.
Each layer depends on the others. The trigger's correctness depends on the hold model. The cron job depends on the trigger being the sole gatekeeper. The waitlist depends on stale holds being cleaned up first. Remove any layer and the system degrades.
Enrollment validation

Before a registration is written, every enrollment is validated against a configurable rule engine:

Schedule Conflicts
Same day, overlapping time, against confirmed rows and active holds.
Prerequisites
Cross-term prerequisite checks before advanced classes.
Concurrent Enrollment
Some classes require co-enrollment in another class.
Teacher Gates
Instructor approval required before enrollment is confirmed.
Each rule is configurable as hard-block or soft-warn with admin waiver support. Rules change between terms without code changes.
No application-level locking. No polling loop in the frontend. Failure recovery is fully automatic. The complexity lives where it belongs: in the database.
Through Line

The database is the source of truth for everything that matters.

Pricing validated at the database on write, never trusted from the client
Seat availability enforced by triggers and timed holds, not application locks
Schema changes are versioned migrations with RLS enforced through every one

The alternative (trust the client, compute on the frontend, use application-level locks) would have been faster to build initially. But every one of these features has a failure mode that client-side logic can't catch: stale quotes, race conditions, partial payment failures, abandoned checkouts. Building at the database layer made each failure mode explicit, testable, and recoverable.

Engineering Snapshots

Key technical implementations from the challenges above.

Payments · Reliability
Dual-Guard Idempotency
Duplicate webhooks are caught by two independent guards: an application-level terminal state check and a database-level conditional UPDATE (WHERE status = 'pending'). The webhook is the sole writer of confirmation state.
2
Guards
0
Duplicate confirmations
Security · Defense-in-Depth
Three-Layer Security Model
Three independent enforcement points for children's enrollment data. If any one layer is bypassed, the other two prevent unauthorized exposure.
L1
Postgres RLS
Data isolation at the row level
L2
Next.js Middleware
Navigation guards per route
L3
Server-Action Guards
Permission re-check per mutation
Enrollment · Business Logic
Configurable Validation Engine
Every enrollment is validated against schedule conflicts, prerequisites, concurrent enrollment requirements, and teacher gates. Each rule is configurable as hard-block or soft-warn, with admin waiver support. Rules change between terms without code changes.
4
Rule types
2
Enforcement modes
Quality · Testing
Payment Path Coverage
42 unit tests covering all payment terminal states, concurrent webhook delivery, and end-to-end confirmation flows. No untested path exists between a family's payment and their enrollment confirmation.
42
Unit tests
100%
Payment paths
Architecture

Full Schema — 65 Tables, 10 Domains

Every table in the production schema, organized by domain. Arcs are sized by table count. Chords show cross-domain foreign key coupling. Hover any arc to explore.

65 tables · 10 domains · 40 cross-domain FK flows
65
Tables
10
Domains
40
Cross-domain FKs
Design Decisions

Palette & Design Rationale

Client Portal

The client portal leads with a near-black hero — it reads as a stage before the first word registers, placing the studio's 30-year identity front and center. The violet-to-rose gradient on ‘Center Stage’ is the only decorative moment. Below, a cool lavender-tinted off-white grounds the enrollment flows.

Stage Black
Hero background
#18141c
Stage Violet
Gradient start
#b89af0
Stage Rose
Gradient end
#e882c2
Portal Surface
Content background
#f4f2f9
Admin Portal

The admin surface flips to a light, high-contrast environment. Staff spending hours in the dashboard need legibility over atmosphere. Deep crimson anchors primary actions without competing with data-dense tables.

Deep Crimson
Primary actions
#8b1a2c
Light Bone
Table backgrounds
#f5f4f1
Dark Slate
Data typography
#1e2530
Clean White
Content surfaces
#ffffff