Setpoint Academy proposal · Earlier todayAll deliverables · Scope · Mockups

Setpoint Academy — feature audit

Repo: /Users/Alvaro/cowork/setpoint (read-only audit, 2026-09-16). All paths relative to that root.

Totals: HAVE 12 · PARTIAL 12 · MISSING 6 (30 rows).

Feature matrix

# Feature Status Evidence Note
1 Class templates (group/private, max pax, duration, price) HAVE supabase/migrations/00000000000000_initial_schema.sql:3213 (classes: class_type CHECK private|group, min_pax/max_pax, duration_minutes, num_courts, price_per_pax_amount, focus_area_id); apps/admin/src/app/[facility]/academy/classes/page.tsx; apps/admin/src/app/api/admin/classes/[id]/route.ts; e2e apps/admin/e2e/tc-036-academy-classes.e2e.ts Complete, with a per-lesson price_per_pax_amount_override on lessons.
2 Recurring lesson scheduling (weekly slots) PARTIAL apps/admin/src/app/api/admin/lessons/recurring/route.ts; packages/common/src/recurrence.ts (computeOccurrenceDates, RECURRENCE_SOFT_CAP_DAYS); supabase/migrations/20260724064533_add_lessons_recurring_group_id.sql Batch-generates occurrences from {start_date, end_date, days_of_week}, mirroring the court-booking recurrence contract. Group classes only — private lessons are excluded because 20260619035520_bypass_group_lesson_coach_availability_check.sql scopes the availability bypass to class_type='group'. No standing "slot" entity; the series is a materialized batch of lessons.
3 Weekly planning calendar (drag / reschedule) PARTIAL apps/admin/src/app/[facility]/academy/planning/page.tsx → apps/admin/src/components/admin/academy-planning-client.tsx (day/week/month views, per-coach tabs, COACHING_ONLY category filter, WeekCalendarGrid); e2e apps/admin/e2e/tc-266-academy-planning.e2e.ts Read-and-filter calendar. No drag-and-drop: zero drag/dnd hits in the planning client. Rescheduling happens in the lesson edit dialog, which emits a modified event (supabase/migrations/20260813071629_emit_lesson_modified_on_reschedule.sql, court change in 20260813074246).
4 Coach assignment + coach availability conflict checks HAVE apps/admin/src/app/api/admin/lessons/[id]/assign-coach/route.ts; apps/admin/src/app/api/admin/coaches/[id]/schedule/route.ts; migrations 20260522082857_coach_availability_booking_conflict_gate.sql, 20260527025804_coach_availability_bounds_and_dow.sql, 20260527073858_coach_availability_per_court_exceptions.sql, 20260527141545_coach_availability_blocks_courts.sql, 20260530074244_coach_overlap_consistency.sql, 20260601043321_coach_lesson_windows_for_date.sql; ~20 e2e specs tc-136–tc-163, tc-242–tc-250; supabase/snippets/coach_availability_booking_conflict_evals.sql The most heavily specified and tested area of the module. 20260724085951_lessons_optional_coach.sql permits an unassigned lesson; group lessons deliberately bypass the gate.
5 Court auto-booking for lessons HAVE apps/admin/src/app/api/admin/lessons/_lib/create-single-lesson.ts; migrations 20260508164245_coach_court_reservation.sql, 20260525163034_skip_booking_events_for_lesson_bookings.sql, 20260731102853_lesson_availability_override.sql, 20260514161348_schedule_cleanup_expired_lesson_holds.sql; e2e tc-025-multi-court-lesson, tc-026-multi-court-verification, tc-132-coach-court-reservation, tc-264-lesson-availability-override Creating a lesson reserves its court(s) as bookings. Multi-court honoured via classes.num_courts. Lesson bookings are excluded from the ordinary booking event stream on purpose.
6 Student register (profile, phone, packages, sessions remaining) HAVE View academy_students in supabase/migrations/20260717050723_add_academy_students_view.sql (derived, service-role only, deliberately not granted to anon/authenticated); apps/admin/src/app/[facility]/academy/students/page.tsx and students/[userId]/page.tsx; apps/admin/src/components/admin/students-list.tsx, students-roster-panel.tsx, student-detail-client.tsx (tabs Overview / Packages / Lessons / Groups / Notes & tags); RPC get_player_package_remaining_sessions (20260803101048); e2e tc-255-academy-students-tab, tc-261-students-v2-filters-pagination A "student" is any customer rostered on a lesson or holding an academy package — no separate student table, so no drift. Filters, search and pagination present.
7 Groups (academy_groups): level, roster, coach PARTIAL supabase/migrations/20260722032430_add_academy_groups.sql (academy_groups + academy_group_members); apps/admin/src/app/api/admin/academy-groups/[id]/members/[userId]/route.ts; apps/admin/src/components/admin/academy-groups-panel.tsx; apps/admin/src/app/[facility]/academy/students/groups/[id]/page.tsx; e2e tc-262-academy-groups v1 is roster + free-text level + optional coach_id. The migration header states explicitly: not linked to a class or schedule, no auto-booking, membership is many-to-many, and per-group "classes done/remaining" is deliberately NOT tracked — it is derived from package counters instead.
8 Attendance / check-in per lesson player HAVE lesson_players.checked_in_at (initial schema) and lesson_players.attended boolean (20260622033935_add_feedback_to_lesson_players.sql); apps/admin/src/app/api/admin/lessons/[id]/players/route.ts and [id]/students/route.ts; apps/admin/src/components/admin/lesson-roster-panel.tsx:181 (handleCheckIn); coach write policy 20260721091646_coaches_can_check_in_lesson_players.sql (UPDATE only, scoped to the coach's facility); Flutter apps/calendar_pwa/lib/screens/qr_checkin_screen.dart; e2e tc-113-lesson-per-student-checkin, tc-114-terminal-lesson-per-student-checkin, tc-021-admin-coach-booking-checkin Who can mark it: owner/admin/manager through the admin roster panel, and coaches through the staff PWA QR screen. Two overlapping fields exist — checked_in_at (check-in) and attended (set in the coach feedback flow). Surfaced in the admin roster panel and the PWA; not surfaced in academy reports.
9 Absence / cancellation by the player PARTIAL cancellation_policies in the initial schema (free_cancellation_hours default 24, applies_to_bookings, applies_to_matches); apps/admin/src/app/api/v1/me/bookings/[id]/cancel/route.ts; cancel_booking_with_refund() and the leave-deadline logic at 00000000000000_initial_schema.sql:565 / :660 / :724; session refund reason in package_session_transactions (20260824200640); 20260807064145_cancel_booking_cancels_add_ons.sql; e2e tc-045-cancellations, tc-150-lesson-cancel-cascades No academy lane. apps/admin/src/app/api/v1/me/lessons/[id]/route.ts exposes GET only, and cancellation_policies has no applies_to_lessons flag. A student cannot self-cancel a lesson or mark an absence; staff cancel and refund the session manually, and the cut-off rules only govern court bookings and matches.
10 Waitlist for full groups MISSING Waitlist exists only for programs: apps/admin/src/app/api/v1/programs/[id]/enroll/route.ts, apps/admin/src/app/api/v1/me/enrollments/route.ts, apps/admin/src/app/api/admin/programs/[id]/enrollments/route.ts, apps/admin/src/components/admin/program-enrollments-list.tsx, apps/web/src/app/(public)/[facility]/programs/[programId]/page.tsx Nothing waitlist-shaped on lessons, lesson_players or academy_groups. A full class simply refuses the join (lesson_capacity_exceeded in 20260824200640).
11 Makeup / recovery credits MISSING Zero hits for makeup / make_up / make-up / recovery across supabase/ and apps/ The closest primitive is a manual admin_adjust entry on package_session_transactions via adjust_package_sessions(), which raises unless the operator supplies a reason. No automatic credit for a missed session.
12 Level assignment / level progression history PARTIAL player_ratings + upsert_consumer_level RPC (20260602065007_add_multi_leveling_consumer_level_type_and_upsert_rpc.sql, service-role only, described as READ-ONLY external ingest); apps/admin/src/app/api/v1/me/levels/route.ts (GET, PATCH); lesson_players.suggested_player_level (20260707095831); academy_groups.level free text; RLS policy "Admins and coaches can manage player ratings" (initial_schema:9393) Levels are a current value per (user, source, level_type), mostly ingested from OpenRally consumers. The AI-suggested level carries a COMMENT saying it is cosmetic/advisory only, not linked to anything. No level-history table, no academy-side promotion flow, no group-level progression.
13 Coach feedback per player (structured, AI, video) HAVE lesson_players: coach_feedback, coach_rating, attended (20260622033935); technique_focus, tactical_work, session_focus, rpe (1-10), suggested_player_level (20260707095831); feedback_generated_by, feedback_reviewed_at (20260706094102); RPC apply_coach_feedback_to_lesson_player (20260707082200, extended for structured fields in 20260707100830); guards in 20260707043836_ai_feedback_ownership_guards_and_openrally_review_gate.sql; apps/admin/src/app/api/admin/lessons/[id]/ai-feedback/route.ts; apps/admin/src/app/api/admin/lessons/[id]/analyze-video/route.ts (OmniGPT native video understanding on a YouTube URL, maxDuration = 60, CORS for the staff PWA); Flutter apps/calendar_pwa/lib/screens/coaching_feedback_flow_screen.dart; e2e tc-253-ai-coaching-feedback Video analysis drafts notes only and never writes lesson_players — the coach reviews and edits before saving, preserving the "coach always has final say" rule.
14 Player-visible progress / evolution MISSING apps/admin/src/app/api/v1/me/lessons/route.ts, me/levels, me/packages, me/elo return schedule, a level value and balances; apps/web/src/app/(public)/[facility]/academy/* is catalogue plus booking Coach feedback, RPE, technique and tactical notes are never exposed on any player-facing route. There is no progress or evolution view in apps/web.
15 Packages: subscription vs session bank, billing fields HAVE lesson_packages.package_type CHECK session_bank|recurring, billing_period widened to weekly/monthly/quarterly/annual and billing_mode added in 20260824200650_academy_billing_cycles.sql; player_packages.billing_mode, next_billing_date, billing_anchor_day (1-31) plus partial index idx_player_packages_next_billing; package_items with per-class-type buckets (20260709060349_package_items_any_class_type.sql); apps/admin/src/app/api/admin/lesson-packages/[id]/route.ts, apps/admin/src/app/api/admin/player-packages/; apps/admin/src/components/admin/package-form-dialog.tsx, player-packages-list.tsx, package-session-activity.tsx; e2e tc-022-admin-package-full-flow, tc-039-academy-packages billing_mode defaults to manual on every existing row (DEC-327), so automatic billing is opt-in per package and dormant today. billing_anchor_day is stored separately from next_billing_date so clamping 31 Jan to 28 Feb does not permanently drift the anchor.
16 Automatic monthly billing (process-package-billing) HAVE (dormant) supabase/functions/process-package-billing/index.ts (537 lines); dispatcher dispatch_package_billing() in 20260824200650_academy_billing_cycles.sql (hourly pg_cron, Vault internal_function_secret, due-date tested against the facility-local date, not UTC) What it does: rolls the cycle for billing_mode='automatic', active packages whose next_billing_date has arrived; computes period start/end from the template's billing_period and the anchor day; inserts a payment_requests row for the period with a due date from academy_payment_settings.default_due_days; queues reminders on the configured offsets (default [-3, 0, 3, 7], template key package_payment_reminder); advances next_billing_date. What it does NOT do: grant sessions, and mint Stripe links — a package carrying stripe_subscription_id is skipped entirely so Stripe's own invoice.paid keeps owning it (AC-41). Idempotent through the unique index payment_requests_subject_period_key, so repeated hourly delivery is harmless.
17 Payment requests + reminders (channels, templates, transport) PARTIAL supabase/migrations/20260824200644_academy_payments_core.sql creates payment_requests (status draft/sent/viewed/paid/cancelled/expired, provider, checkout_id unique, checkout_url, due_date, period_start/period_end), payment_reminders (channel CHECK email|whatsapp, template_key, status queued/sent/sent_manual/failed/skipped, provider_message_id, persisted error, attempt, idempotency index on request+template+schedule) and message_templates (facility row falls back to a system default, subject only allowed on email); 20260902172910_academy_reminder_idempotency_channel.sql; apps/admin/src/app/api/admin/academy-payments/reminders/route.ts, settings/route.ts, summary/route.ts, _lib/feed.ts; apps/admin/src/components/admin/academy-payments-reminder-panel.tsx, academy-payments-settings-tab.tsx; supabase/functions/send-payment-reminder/index.ts; dispatcher dispatch_payment_reminders() Email is a real transport (Resend, via apps/admin/src/lib/messaging/providers/email.ts and the edge function's render-or-transport modes). WhatsApp has no automated transport: apps/admin/src/lib/messaging/providers/whatsapp-manual.ts builds a wa.me deep link that the operator clicks and records status sent_manual; providers/whatsapp-cloud.ts is a deliberate stub that throws rather than silently falling back, because a Meta Business account, a dedicated sending number and Meta-approved templates are unresolved business decisions (DEC-323, openQuestions Q1/Q2). The edge function has no WhatsApp path at all, and it mints no checkout links — without a provider it falls back to the facility's pay-at-club instructions, which is the path every facility takes today since all have payment_provider IS NULL.
18 POS bridge (academy_payment_pos_bridge) HAVE supabase/migrations/20260901170806_academy_payment_pos_bridge.sql (function record_academy_payment_in_pos_row, AFTER INSERT trigger on payments); reversal path 20260902172043_academy_payment_pos_reversal.sql; supabase/snippets/academy_pos_bridge_evals.sql Records: a completed pos_orders row on the new channel academy (deliberately not masquerading as terminal), with one line item and one captured payment, at the facility's oldest active POS location, stamped with the facility-local business date. Fires only for payments.type in (package,lesson), status succeeded, amount > 0, and a real payment_method (skips credits, package, pending as entitlement consumption or not-yet-arrived money). Same transaction as the money-truth row, idempotent on payments.id via the POS tables' partial-unique source_id indexes. It stores no Stripe charge or payment_intent id — those live on player_packages.stripe_payment_intent_id and payment_requests.checkout_id.
19 Proration of a package charge MISSING Every prorat* hit is court pricing: supabase/migrations/20260731032100_cross_window_pricing.sql, 20260717080000_add_pricing_mode_to_pricing_rules.sql, supabase/snippets/cross_window_pricing_evals.sql, apps/admin/src/components/admin/pricing-rule-dialog.tsx, apps/admin/src/lib/add-ons/index.ts, apps/admin/e2e/setup-booking-data.ts A student joining mid-cycle is billed a full period. No partial-period arithmetic anywhere in process-package-billing or the package routes.
20 Reconciliation Stripe vs recorded payments PARTIAL supabase/migrations/20260605094626_events_payment_reconciliation.sql + supabase/snippets/events_payment_reconciliation_evals.sql; supabase/snippets/academy_pos_bridge_evals.sql; payment_requests.checkout_id unique index so a replayed webhook cannot settle two rows; supabase/functions/pos-stripe-webhook/index.ts; apps/admin/src/app/api/v1/payments/checkout/route.ts; apps/docs/src/data/handoff-bug-019-stripe-webhook.md; e2e tc-076-cross-app-payment-verification, tc-106-openrally-payment-sync, tc-278-checkout-syncs-booking-payment-status Reconciliation exists for OpenRally booking events and for the POS mirror, as SQL eval snippets run by tc-000-sql-evals.e2e.ts. There is no Stripe-vs-payments reconciliation report in the admin UI, and no scheduled drift check. Worth noting alongside this: CLAUDE.md:86 records that Stripe runs on test keys in production intentionally.
21 Attendance vs payment cross-check MISSING Check-in lives on lesson_players (checked_in_at, attended); money lives on payments / payment_requests; apps/admin/src/app/api/admin/academy-payments/summary/route.ts and _lib/feed.ts contain no attendance join Nothing pairs "attended" against "paid" — an unpaid student who attends raises no flag. The nearest signal is Package Health utilization in academy reports (sessions consumed vs total), which measures consumption, not payment.
22 Reports (academy-reports, report targets) HAVE apps/admin/src/app/api/admin/academy-reports/route.ts; apps/admin/src/components/admin/academy-reports-dashboard.tsx; apps/admin/src/app/[facility]/academy/reports/page.tsx; academies.targets JSONB from 20260604082325_academy_report_targets.sql; apps/admin/src/app/api/admin/group-report-subscriptions/ and 20260820130354_group_report_subscriptions.sql; e2e tc-040-academy-reports KPIs: total revenue, package revenue, direct/walk-in revenue, deferred revenue, daily revenue trend chart, lessons count, per-coach filter, day-of-week distribution, package health with underutilized monthly plans (sessions_consumed/sessions_total, utilization_pct), and progress bars against editable monthly targets (revenue_min/revenue_ideal, lessons_min/lessons_ideal). All date maths run on facility-local boundaries.
23 Leads / CRM / interest registrations PARTIAL apps/admin/src/app/api/admin/crm/ — customers (notes, tags, timeline, coaching, discounts, merge, delete), segments (+preview, members), tags, groups, campaigns (+send, ai-design, upload-image, webhook), discount-rules, email-settings, templates, pos-catalog-items, analytics; apps/admin/src/app/[facility]/crm/customers/[id]/page.tsx; apps/docs/src/data/ac-crm-plan.ts, plan-crm-segments.ts, plan-crm-campaigns.ts; e2e tc-041-customers A full customer CRM, but no lead or enquiry entity: the only lead matches in the repo are inside business-report SQL. Someone who has enquired but never booked or bought has no record, so the academy has no top-of-funnel.
24 Messaging: 1:1 or group messages, push, in-app PARTIAL apps/admin/src/app/api/admin/crm/campaigns/[id]/send/route.ts (Resend audiences, lazily created per facility, segment-filtered bulk email); apps/admin/src/lib/messaging/ (email + whatsapp_manual + whatsapp_cloud stub); apps/admin/src/app/api/admin/message-templates/; crm_email_settings; a dozen transactional edge functions (send-lesson-payment-link, send-lesson-package-payment-link, send-program-enrollment, …) Bulk email campaigns, transactional email, and manual WhatsApp deep links. No push notifications, no in-app notification centre, no 1:1 message thread, and no "message this group" action on academy_groups.
25 Student-facing app / web PARTIAL apps/web/src/app/(public)/[facility]/academy/{page,packages,lessons,coaches/[id]}/page.tsx, plus profile, credits, bookings, memberships, programs, matches; apps/admin/src/app/api/v1/me/ — lessons, lessons/[id], packages, levels, credits, bookings (+[id]/cancel), membership (+cancel), enrollments, elo, profile, avatar; join/purchase routes api/v1/lessons/[id]/join, api/v1/lesson-packages/[id]/{purchase,subscribe,confirm-purchase}; api/v1/facilities/[slug]/{classes,coaches,lessons/available,lesson-packages}; e2e tc-017-package-purchase-web, tc-062-web-academy A student can browse classes, coaches and packages, join a lesson, buy or subscribe to a package, and see their upcoming lessons, package balances and level. They cannot cancel a lesson, see coach feedback, or see progress.
26 Coach-facing app (calendar_pwa) HAVE Flutter web app, apps/calendar_pwa/lib/screens/: calendar_screen.dart, coach/coach_schedule_screen.dart, coach/availability_screen.dart, coach/book_session_screen.dart, qr_checkin_screen.dart, coaching_feedback_flow_screen.dart, expense_submit_screen.dart, expense_review_screen.dart, my_expenses_screen.dart, login_screen.dart; apps/calendar_pwa/CLAUDE.md; supporting RPCs 20260707082200, 20260707100830; edge function submit-expense A coach sees their schedule, edits availability (bounds, day-of-week, per-court exceptions), books a session, QR-checks in players, runs the feedback flow including the video-analysis draft step, and files expenses. Deployed as staff-setpoint with --pwa-strategy=none.
27 Roles / permissions in admin PARTIAL user_facility_roles.role CHECK owner|admin|manager|coach|member|guest (00000000000000_initial_schema.sql:5536); facility_contacts.role adds reception/sales/other (:3688); apps/admin/src/lib/auth.ts (requireFacilityAccess); apps/admin/src/app/api/admin/team/; academy RLS in 20260722032430 (owner/admin/manager write, staff read); coach grants 20260609120000_coach_rls_lesson_students.sql and 20260721091646; e2e tc-043-team Four effective staff roles. Academy writes are owner/admin/manager; a coach gets narrowly scoped grants (read their lesson students, write checked_in_at, save their own feedback through an RPC that verifies assignment). No per-action approval workflow other than the discount-approval-code edge function. Caveat: admin API routes use createAdminClient() and bypass RLS, so the route-level requireFacilityAccess check is the real boundary — the policies are defence in depth.
28 Multi-academy / multi-facility support HAVE academies (owner_facility_id) and join table academy_facilities in the initial schema; 20260717063517_add_academy_facilities_unique_academy_id.sql; apps/admin/src/app/api/admin/academies/[id]/facilities/route.ts; every academy table carries both academy_id and facility_id; routes resolve the academy through academy_facilities; e2e tc-260-academy-facility-unique-constraint One academy can span several facilities; a facility maps to one academy (enforced by the unique constraint). Admin routing is facility-first via the [facility] slug segment.
29 Data export (CSV) MISSING (academy) CSV export exists only in POS: apps/pos/src/app/(dashboard)/reports/{sales-report,discounts,sales-taxes,gift-card-liability}/page.tsx, apps/pos/src/app/(dashboard)/items/page.tsx, items/inventory/history/page.tsx Zero csv hits under apps/admin/src. No student list, payment feed, attendance or report export from the academy module.
30 Audit log of admin actions PARTIAL Domain ledgers only: package_session_transactions with actor_id, mandatory reason and a 7-value reason CHECK (20260824200640_package_session_ledger.sql; adjust_package_sessions() raises P0001 on a blank reason); append-only payment reversals through payments.reverses_payment_id with UNIQUE payments_one_reversal_per_original (20260902172043) driven by apps/admin/src/app/api/admin/academy-payments/revert/route.ts, which refuses a revert without a reason and refuses to revert a reversal; payment_requests.created_by, payment_reminders.created_by, message_templates.updated_by; pos_activity_logs (initial_schema:4165, POS only); customer merge/delete audit in 20260705120233 and 20260706041805 Money and session movements are fully attributable and append-only. There is no general admin audit table — no audit_log, no activity_log outside POS — so edits to classes, lessons, groups, coaches or package templates leave no actor trail.

(a) Academy directories

Admin pages under apps/admin/src/app/[facility]/academy: classes, coaches, coaches/[id], lessons, packages, payments, planning, reports, setup, students, students/[userId], students/groups, students/groups/[id], plus the module landing page.tsx. Thirteen page files in total.

Academy-related routes under apps/admin/src/app/api/admin:

(b) Acceptance-criteria and decisions files

Under apps/docs/src/data:

File ACs
ac-academy-payments.ts 53
ac-package-session-lifecycle.ts 44
ac-academy-booking-ux.ts 25
ac-coach-booking-openrally.ts 25
ac-academy-setup-ia.ts 22
ac-checkin-pos-court-fee-pricing.ts 21
ac-coach-discovery-openrally.ts 20
prd-academy-coaches.ts 5

Eight files, 215 acceptance criteria. decisions.ts holds 352 DEC- entries (DEC-320/321/322/323/327 are the load-bearing ones for academy payments). Supporting plans and guides: plan-coach-availability-overhaul.ts, plan-coach-booking-openrally.ts, plan-lesson-per-student-checkin.md, plan-booking-status-payment-checkin.md, plan-checkin-consolidation.md, coach-availability-guide.{md,ts}, ac-coach-availability-blocks-courts-toggle.ts, handoff-future-coach-mapping-automation.md, plus GROUP-COACHING-PLAN.md at the repo root.

(c) Test conventions

Database tests. There is no supabase/tests directory and no pgTAP files — no .test.sql or _test.sql anywhere. Database-level assertions live as eval scripts in supabase/snippets/ (8 files: academy_pos_bridge_evals.sql, coach_availability_booking_conflict_evals.sql, cross_window_pricing_evals.sql, discount_composition_evals.sql, discount_engine_evals.sql, events_payment_reconciliation_evals.sql, program_session_openrally_evals.sql, wallet_evals.sql) and are executed from Playwright by apps/admin/e2e/tc-000-sql-evals.e2e.ts.

Playwright. All specs are apps/admin/e2e/tc-*.e2e.ts under one config, run with cd apps/admin && npx playwright test in the foreground with a timeout at or under 600s; videos land in apps/admin/test-results/ as proof. Test cases are defined by the product owner at https://product-setpoint.vercel.app/test-cases. Shared helpers in apps/admin/e2e/test-helpers.ts export ADMIN_URL, WEB_URL, SUPABASE_URL and getOtpFromMailpit(); setup lives in auth.setup.ts, global-setup.ts, global-teardown.ts, setup-booking-data.ts, seed-crm-data.ts, with flows/ and demos/ subdirectories. Roughly 80 of the specs touch academy, coaching, packages or payments.

.claude/rules/e2e-testing.md requires, for any PR touching apps/admin/e2e/**:

.claude/rules/docs-sync.md requires, for any PR touching apps/docs/src/data/**, apps/apidocs/src/data/**, apps/admin/src/app/api/**, supabase/migrations/** or packages/database/src/**: update versions.ts (including the e2eTests count and feature list), feature-matrix.ts (endpoint counts, new rows), deployment.ts, monthly-planning.ts (goal status), and any prd-*.ts; also CLAUDE.md, apps/apidocs/src/data/api-reference.ts and a regenerated packages/database/src/database.types.ts when affected. Then build both docs apps (cd apps/docs && npm run build, cd apps/apidocs && npm run build) to catch errors. A pre-commit hook warns when docs files are missing.

Other rule files: .claude/rules/migrations.md (one-shot migration rules), coach-availability.md, openrally-integration.md.

(d) PR and post-merge deployment checklist in CLAUDE.md

Feature workflow (line 102), 13 steps: select and sequence → clarify, blocking until zero ambiguity, logging answers as decisions → write ACs → plan → persist AC and plan as ac-<feature>.ts in a chore PR → build in atomic commits → E2E per .claude/rules/e2e-testing.md → self-review the full diff line by line → verify tests pass → sync docs per .claude/rules/docs-sync.md → merge prep → deploy → delete the ac- file in a chore PR.

Line 104 marks step 8 mandatory before merging: run git diff origin/main...HEAD, read every changed line, verify correctness before gh pr merge. Skipping it once shipped a critical bug where an is_active filter hid 129 live items from staff terminals.

Git and PR rules (lines 96-98): branches feature/ fix/ hotfix/ chore/; one feature = one branch = one PR; the pre-commit hook rejects direct-to-main; git fetch origin main && git merge origin/main before PR or merge, watching supabase/migrations/ ordering; merge with gh pr merge --squash --delete-branch, then git checkout main && git pull. The PR description must contain TLDR, root cause, file-by-file changes, reasoning and trade-offs, and agent-context written for a zero-context reader. New columns must be justified with what was checked and ruled out (line 37). A patch that ships ahead of the architectural fix must be labelled a patch in the PR with the follow-up filed (line 33).

Code review (line 117) is mandatory before deploy. Demo before PR (lines 121-123) is mandatory when the PR needs sign-off: a narrated screen recording, or a clear GIF/screenshot walkthrough, attached to the PR or sent with the link. A written summary is explicitly not a substitute.

Post-merge deployment (line 152): Vercel auto-deploys each app independently on merge. Manual steps after merge are: run any new DB migration with supabase db query --linked --file supabase/migrations/<file>.sql (NEW files only — supabase db push and supabase db reset --linked are permanently banned per line 20), deploy edge functions with supabase functions deploy <name> --project-ref utommwznukufegwhltqw, and for OpenRally cd protocol/server && flyctl deploy --remote-only. Then verify admin-setpoint, front-setpoint, product-setpoint and apidocs-setpoint. Never create a new Vercel project to make a deploy work (line 129 — the CLI paginates and hides projects past the first ~20), and always cd into the app's own directory before a manual deploy, or a root-level vercel --prod will deploy whatever the root is linked to.