BERTH 09 — SOFTWARE SYSTEMS

yogaEngine

Next.js 15 · Clerk · Supabase · Stripe · iframe embed

Self-hosted class booking and membership platform for Santa Barbara Yoga Collective. Drops into their existing Squarespace site as a direct replacement for Acuity Scheduling — same embed points, new src. The studio owns its booking data, checkout branding, and member records for the first time.

Active — M4 of 7 Next.js 15 TypeScript Clerk Supabase Stripe
BUILD PROGRESS — 7 MILESTONES
M1
Schema
M2
Auth
M3
Public
M4
Admin
M5
Live DB
M6
Email
M7
Launch
STACK
FRAMEWORK
Next.js 15 App Router
React 19 · TypeScript strict
Route groups: (public) (member) (admin)
Tailwind CSS 4 + CSS custom properties
shadcn/ui (Radix UI primitives)
Lucide icons
BACKEND & AUTH
Clerk @clerk/nextjs v6
Supabase PostgreSQL · Prisma ORM
11 tables — all live, schema pushed
Stripe v22 (API 2026-04-22.dahlia)
Resend — wired, not yet active
TYPOGRAPHY
Fraunces (variable serif) — display
Manrope (geometric sans) — UI text
Both via next/font/google
Exposed as --font-display / --font-sans
Design tokens in globals.css + Tailwind config
INFRASTRUCTURE
Vercel (Next.js + serverless functions)
Cloudflare DNS — CNAME to Vercel
Squarespace — embed host, not modified
NEXT_PUBLIC_APP_URL for absolute URLs
in Stripe sessions + webhook redirects
ARCHITECTURE

The core constraint is that the studio's Squarespace site stays intact. Acuity Scheduling is an iframe embed. This system is also an iframe embed. Replacing Acuity means updating the src attribute on two existing embed blocks — the public schedule and the member portal. No Squarespace changes, no SEO disruption, no marketing content rebuilt.

SQUARESPACE studio's marketing site <iframe src= "…/schedule"> <iframe src= "…/portal"> src is the only change NEXT.JS APP (public) · (member) · (admin) schedule · class detail · cart checkout · member portal · admin demo mode: mock checkout active seed data → schedule + bookings localStorage → cart + membership Clerk PKCE · token rotation · MFA Supabase + Prisma 11 tables · live · not yet queried Stripe v22 sessions · subscriptions · webhooks

Route groups ((public), (member), (admin)) are purely organizational — they add no URL prefix. middleware.ts classifies every incoming route and hands off to Clerk for session validation on protected paths. The admin layout server component checks an ADMIN_EMAILS allowlist and domain before rendering. Membership state currently lives in localStorage under key sbcyc-membership-v1; both the cart and membership move to Prisma queries in M5.

The schedule is driven by a WEEKLY_PATTERN — 18 recurring class slots, each with day-of-week, time, instructor, room, capacity, and pricing. getSessionsForWeek(weekOffset) materializes that pattern into concrete dates for any given week. One-time sessions carry a cancelsPatternSlot field to model schedule exceptions without touching the underlying pattern. This makes the calendar view (which needs 2+ months) a loop over offsets 0–8 with no database or API call.

ENGINEERING DECISIONS
NOTABLE PROBLEMS
Route group duplicate segment conflict

Route groups are organizational — they don't add a URL prefix. That means app/(admin)/schedule/page.tsx and app/(public)/schedule/page.tsx both resolve to /schedule, which Next.js rejects as a duplicate route at build time. The fix is an explicit admin/ subfolder inside the (admin) group: app/(admin)/admin/schedule/page.tsx resolves to /admin/schedule. The error message doesn't immediately surface route groups as the cause, and the build fails hard rather than warning.

Stripe instantiation crashing the build

The Stripe webhook route originally instantiated new Stripe(key) at module load time. Next.js evaluates route modules during the build to generate the route manifest, so a missing key crashes the build before the server starts. The fix: import a shared lib/stripe.ts module that returns null in demo mode, and check isDemoMode at the top of the handler before touching the Stripe client. The module never instantiates when no key is present.

Clerk dev-browser requirement inside an iframe

In development, Clerk's session token setup requires a real browser — it sets a dev-browser cookie via JavaScript that isn't present in curl requests. Protected routes return a 404-shaped response (Clerk rewrites the request to an internal URL that Next.js doesn't find) rather than a 401. The routes are correct; the server is healthy. The pattern only becomes clear once Clerk's dev-browser mechanism is understood. In production with real cookies, this doesn't occur.

Next.js 15 breaking change: params and searchParams are Promises

In Next.js 15, params and searchParams in server components are now Promises and must be awaited before use. In v14 they were synchronous. The change produces silent type errors — TypeScript doesn't flag the missing await in all cases because the unresolved Promise still passes type-checks in some contexts — and the runtime behavior is wrong values rather than thrown exceptions. Every dynamic route and search-param-reading page needed an audit after this was identified.

CURRENT STATUS — M4 OF 7
WORKING Full public booking flow (schedule → class detail → cart → checkout → confirmation). Guest checkout. Membership purchase and activation. Member portal (booking history, membership management). Admin panel (dashboard, schedule view, booking table, instructor management, promo codes, embed code generator, member grant/migration tool).
DEMO MODE Stripe (mock checkout at /checkout/mock). All schedule and booking data (seed files). Membership state (localStorage). Supabase connected and all 11 tables live; not yet queried.
NOT BUILT Email notifications (Resend, M6). Live Prisma queries replacing seed data (M5). Stripe webhook DB writes (M5). Waitlist auto-promotion. Admin CRUD that persists to the database.
NEXT M5: replace seed data reads with Prisma queries, wire Stripe webhook to write booking records, move cart and membership from localStorage to the database. The production gap is well-defined: set real Stripe keys, run the webhook, swap seed reads for live queries.