Prodemy

Admin Guide

Approval queues, user management, two-factor reset, club management, platform config, scheduled changes, admin roles, drill library, content moderation, financials, admin financial dashboard, billing, chargebacks, offline reconciliation, terms compliance, platform terms authoring, SMS control center, SMS monitoring, audit log, cron jobs, invoicing, feature flags, chatbot KB management

Last updated June 27, 2026

Admin Guide#

This guide is for Prodemy platform admins. Unlike files 01–09 which are user-friendly, this one is allowed to reference feature IDs (FEAT-XXX) and API routes — admins are technical enough to work with them and sometimes need to find specific features by ID.

Source of truth for everything architectural:

  • CLAUDE.md at the repo root — single source of truth, updated daily.
  • project-state.json at repo root — next feature numbers, in-progress features, platform rules.
  • docs/ — feature implementation plans, bug logs.

Admin dashboard#

Dashboard at /dashboard/admin. Sidebar: Overview, Approvals, Users, Clubs, Coaches, Content, Financials (+ Admin Financial Dashboard at /dashboard/admin/financial), Billing (club invoices + Chargebacks), Offline Reconciliation, Compliance (club T&C) + Platform Terms, Config (incl. SMS at /dashboard/admin/config/sms and Chatbot KB at /dashboard/admin/config/chatbot), Audit log, Settings, Notifications.

Summary cards#

Total users, active clubs, monthly platform revenue, pending approvals count. Engagement snapshot below — DAU, drills completed, videos uploaded, lane bookings. User breakdown by role.

Approvals queue (unified)#

Four tabs:

  • Club registrations — new clubs awaiting approval. Shows owner details, locations, lanes, Stripe Connect status. Approve / reject.
  • Drill submissions — club-submitted custom drills for the master library. Approve / reject / edit.
  • Reported posts — flagged community posts (already auto-hidden). Restore (false flag) or permanently remove (with optional author warning or suspension).
  • Flagged coach reviews — reviews coaches have disputed. Keep or remove.

Each tab shows a pending count badge in the sidebar.

User management#

/dashboard/admin/users. Searchable table with role badges, status, actions.

Per-user actions#

  • Edit profile — update any user field for support cases.
  • Suspend — disables login, hides profile. User sees "Account suspended." Reversible.
  • Delete with cool-off — 7 days for users, 14 days for clubs. All affected parties notified. Admin can cancel during cool-off.
  • Impersonate — log in as any user for troubleshooting (read-only, logged to audit trail).
  • Verify coach — platform-level verification separate from club verification.
  • Change role — opens a role change modal. Shows current role badge, role selector, red SOLE_CLUB_OWNER block if applicable (with eligible transfer targets from the ClubCoach join table).

Role change restrictions#

Role changes fire USER_ROLE_CHANGED notification, log USER_ROLE_CHANGED audit action, and create an ActionSnapshot for revert.

Blocked changes:

  • Parent/Student → Coach/Club Owner (role conflict).
  • Coach/Club Owner → Parent/Student (role conflict).
  • Club Owner who is the sole owner of an active club — returns 400 SOLE_CLUB_OWNER with eligible target coaches.

Delete cascade rules#

  • User delete — profile hidden, login disabled, posts hidden, drill history anonymized. If parent deleted, children reassigned or suspended.
  • Club delete (14-day cool-off) — memberships terminated, bookings cancelled with refunds, tournaments cancelled, coaches unlinked.
  • Coach delete (7-day cool-off) — sessions cancelled with refunds, students notified, reviews archived.

All deletes trigger email + in-app + push notifications (web/PWA push live; native FCM/APN pending) to entity + all affected parties + initiating admin.

Club management#

/dashboard/admin/clubs. Master registry — every club on the platform.

Per-club actions#

  • Approve — transitions a PENDING club to ACTIVE. Creates ClubFeeAgreement (FEAT-103), sets Club.approvedAt (used for ANNIVERSARY billing day, capped at 28), creates platform-side Stripe Customer on ClubOwnerProfile.stripeCustomerId if FIXED_MONTHLY or HYBRID.
  • Reject — rejects a PENDING club registration with a reason.
  • Suspend — sets Club.status = SUSPENDED. Suspended clubs block new enrollments, bookings, and tournament registrations via assertClubNotSuspended() calls at each route. Existing members keep access. Admin can reactivate.
  • Reactivate — returns a SUSPENDED club to ACTIVE.
  • Edit — update club fields for support cases.
  • Feature flags — modify via ClubFeatureGrant records. Requires SUPER_ADMIN.

Suspension vs reactivation#

Requires SUPER_ADMIN role. Suspend reason is required — empty reason returns 400 with validation error. Reactivate returns a suspended club to ACTIVE immediately; renewal cron automatically skips suspended clubs so reactivation doesn't trigger retroactive billing.

Club Fee Agreement (FEAT-103)#

Created at club approval. Stores the per-club platform fee model:

  • feeModel: PERCENTAGE, FIXED_MONTHLY, or HYBRID.
  • commissionRate: percentage taken from each transaction (PERCENTAGE and HYBRID).
  • hybridCommissionRate: percentage for HYBRID mode (as of FEAT-104 Phase 5A).
  • fixedMonthlyFee: flat monthly fee for FIXED_MONTHLY and HYBRID.
  • prorateFirstInvoice: boolean toggle at approval time.

Club.feeModel is a denormalised nullable field mirroring feeAgreement.feeModel for fast list queries. Don't edit it directly — always update through ClubFeeAgreement.

For Stripe application_fee_amount logic, always call resolveStripeApplicationFeeRate(agreement) from @/lib/club-fee. Returns commissionRate for PERCENTAGE, hybridCommissionRate for HYBRID, null for FIXED_MONTHLY. Never pass application_fee_amount: 0 — omit the field entirely when null.

Legacy clubs without a feeAgreement fall back to PlatformConfig.defaultCommissionRate.

Platform config#

/dashboard/admin/payment-config. Platform-wide defaults.

Configurable settings#

  • Default commission rate (platform standard is 3% for PERCENTAGE/HYBRID).
  • Default Stripe fee handling (PASS_TO_PARENT is the default).
  • Default grace period (days) — platform default is now 0 (no grace).
  • Default auto-lock threshold (days) — platform default is now 0 (never auto-lock).
  • Default offline processing fee rate + enabled flag + platform/club split (default 50/50) — FEAT-118 (and MOD-01/02/03). Rate changes are scheduled (forward-only); the enabled flag flips immediately as a kill switch.
  • Minimum refund window (hours — platform minimum).
  • Student plan prices (platformSubPriceBasic, platformSubPricePro).
  • Feature flags (platform-level).

Scheduled changes#

Commission rate and Stripe fee handling changes are never applied immediately. They create ScheduledChange records with an effectiveDate. The cron at /api/cron/apply-scheduled-changes applies them when effectiveDate ≤ now.

The admin UI shows an amber banner + date picker when either scheduled field has drifted from saved values. handleSaveDefaults sends only changed scheduled fields plus effectiveDate. Surfaces SUPER_ADMIN_REQUIRED 403 if the admin isn't SUPER_ADMIN.

Admin roles#

  • ADMIN role — regular admin. All operations except managing other admins, critical config, and explicitly gated SUPER_ADMIN fields.
  • ADMIN + adminRole: "SUPER_ADMIN" — can do everything.

SUPER_ADMIN required for:

  • Commission / pricing changes.
  • Feature flag modifications (ClubFeatureGrant).
  • Club suspend / reactivate.
  • Revert actions (14-day revert window on audit log entries).
  • Role changes on protected fields.

Use isSuperAdmin(user) from @/lib/admin-auth for gating.

Drill library management#

/dashboard/admin/drills. Master library of 1,200+ drills across 8 activities (FEAT-076).

Drill categories per activity#

Cricket has 5 categories: BATTING, BOWLING, FIELDING (WK merged in), FITNESS, MENTAL. CHALLENGE was archived in FEAT-076. Legacy WICKET_KEEPING and CHALLENGE code lives in src/lib/legacy/cricket-drill-legacy.ts.

Other activities have activity-specific categories set via the ActivityType enum and customCategory field on Drill.

CRUD actions#

  • Add drill — inline form below the table. Title, description, category, skill level, duration, video URL.
  • Edit — inline form pre-populated. PUT /api/admin/drills/[id], partial update pattern.
  • Archive (soft delete) — DELETE /api/admin/drills/[id], sets status to archived. Master drills are protected from hard delete.

Bulk operations#

  • Bulk upload (FEAT-025) — GET /api/admin/drills/bulk-upload returns drill_template.xlsx. POST with multipart form data. Validates category (BATTING/BOWLING/FIELDING/FITNESS/MENTAL — WICKET_KEEPING and CHALLENGE rejected as of FEAT-076), skill level, title. Creates with source: MASTER. Returns { created, total, errors[] }.
  • Bulk edit (FEAT-026) — PUT /api/admin/drills/bulk. Update category, skill level, or status across multiple drills.
  • Bulk delete — DELETE /api/admin/drills/bulk. Archives multiple drills at once.

Multi-select via select-all checkbox and per-row checkboxes. Bulk action bar shows selected count.

Club submission review#

Club owners can submit custom drills to the master library. These appear in the Approvals queue → Drill submissions tab. Admin approves (promotes to MASTER), rejects (keeps as club-only), or edits then approves.

Platform analytics and financials#

/dashboard/admin/financials. Platform-level revenue, commission, ARPU.

Financial dashboard#

  • Monthly revenue by category (class, tournament, workshop, camp, coaching session, lane, membership, subscription).
  • Platform commission earned per category.
  • Revenue share summary per club (which uses PERCENTAGE vs FIXED_MONTHLY vs HYBRID).
  • Transaction volume and ARPU.
  • "Open Stripe Dashboard" button for direct access.
  • Export as CSV or PDF.

Engagement analytics#

  • Drill completion rate.
  • Verification turnaround (median hours from submission to verify).
  • Video upload rate.
  • DAU / WAU / MAU.
  • Retention cohorts.
  • Churn analysis.

Admin Financial Dashboard (FEAT-169)#

A separate, payment-level economics view at /dashboard/admin/financial, built on the canonical Rule #7 filter shell. Every paid payment is broken into Gross / Club payout / Stripe fee / Platform earnings (per-club commission + fee model baked in — never a flat platform-wide rate). It's PAID-ONLY (forced to status = COMPLETED, which covers online and confirmed-offline) and dated by paidAt ("collected in period"). Filters, six summary tiles, a per-payment detail drawer, and an xlsx export (hard-422 over 10k rows). Only amounts + a Stripe ledger id are stored — no new PCI scope.

New admin surfaces (2026 wave)#

The admin guide allows FEAT references; here's the map of newer surfaces and what each does.

SMS Control Center + Monitoring (/dashboard/admin/config/sms)#

  • Control (FEAT-158 → FEAT-160) — a kill switch (CRITICAL-tier confirm + cool-off + audit) and a discoverable, role-bucketed event inventory grid covering every event type. Per-row tier dropdown (DISABLED / OPT_IN / REQUIRED), multi-audience expand, bulk actions, a "🚫 Not recommended" advisory badge, and CSV export. All mutations route through the same audited policy endpoint. This is how you decide which event types may send SMS, per recipient role.
  • Monitoring (FEAT-161 → FEAT-162) — a dashboard over NotificationDelivery SMS rows: summary tiles (sent/failed/skipped/failure-rate), top failure reasons, and a filterable, exportable deliveries table with a read-only detail drawer. (Failure-rate denominator excludes SKIPPED + PENDING.)

Billing, invoices, and chargebacks (/dashboard/admin/billing)#

  • Club invoices (FEAT-104) — issue, void, reissue; platform-wide billing metrics; per-club invoice management.
  • Chargebacks (FEAT-151) — first-class tracking of Stripe card disputes. The recorders are state-only; the single money-moving action — recovering a loss from a club's balance — is a manual SUPER_ADMIN button on a LOST dispute behind a confirm + reason. Nothing in this feature moves money automatically. Filter shell + detail drawer + xlsx export.

Offline reconciliation + abuse flagging (/dashboard/admin/offline-reconciliation, FEAT-118)#

Monthly confirmed/pending/rejected splits with CSV export, a platform-wide offline-payment table, and an anti-abuse heuristic that flags clubs whose offline-rejection rate exceeds threshold over a sliding window (the daily evaluate-offline-abuse cron). Flagged clubs surface on /dashboard/admin/clubs/flagged; resolve manually with a reason.

Terms compliance + Platform Terms (FEAT-119 + FEAT-134)#

  • Compliance (/dashboard/admin/compliance) — read-only, platform-wide view of which clubs' members have accepted each club's own Terms & Conditions (with masked IPs). Admins never author or edit club terms.
  • Platform Terms (/dashboard/admin/platform-terms) — author/publish Prodemy's own platform-wide Terms of Service (rich text or PDF), see which clubs are compliant, and (CRITICAL-tier) record a legacy backfill acceptance for pre-existing clubs.

Two-factor reset (FEAT-165)#

When a user is locked out of 2FA (lost device + backup codes), a SUPER_ADMIN can reset 2FA on their account from User management (after their own step-up check). The whole 2FA feature is web-only at launch and gated by NEXT_PUBLIC_ENABLE_TWO_FACTOR (enabled in prod).

Feature config#

Per-club feature grants (ClubFeatureGrant) still gate features (SUPER_ADMIN). Note: equipment/inventory tracking (FEAT-166) is now on by default for all clubs — toggle it OFF per club here if a club doesn't use it. The one-time registration fee (FEAT-149, platform flag NEXT_PUBLIC_ENABLE_REGISTRATION_FEE) and club-owner "Assign students" (FEAT-159, NEXT_PUBLIC_ENABLE_CLUB_OWNER_ASSIGN) are enabled; whether a registration fee appears is then each club's own choice in its settings.

Audit log#

Immutable log of every admin action. ClubActivityLog and ActionSnapshot have no DELETE or UPDATE endpoints — entries are permanent. 2-year retention minimum. 14-day admin revert window on ActionSnapshot entries.

Fields#

AuditLog (admin-only): adminUserId, actionType (AuditActionType Prisma enum), targetEntityType, targetEntityId, details (JSON, before vs after for config changes), ipAddress, createdAt.

ClubActivityLog (club-scoped): clubId (nullable for platform-level), userId, category (MEMBERSHIP / COACHES / CLASSES / TOURNAMENTS / WORKSHOPS / FINANCIAL / BROADCASTS / FACILITIES / SETTINGS / ACCOUNT), action, options (JSON).

ActionSnapshot (for revert): actionType (plain String, not enum — don't confuse with AuditLog.actionType), entityType, entityId, beforeState, afterState, performedBy, performedByRole, clubId?, revertable, metadata.

New AuditActionType values (Phase 8)#

PLATFORM_CONFIG_CHANGE, USER_ROLE_CHANGED. Both pushes follow the two-step pattern: prisma db push --skip-generate then prisma generate.

Searchable#

Filter by date range, admin user, action type, target entity. Sortable. Exportable as CSV.

Revert window#

ActionSnapshot entries with revertable: true can be reverted via the admin UI within 14 days of the action. Revert creates a new ActionSnapshot showing the reversal. This is how "undo class ending," "undo tournament cancel," and "undo fee change" work.

Cron jobs#

All crons at /api/cron/* protected by CRON_SECRET header. Scheduled in vercel.json.

Active crons#

The crons live under /api/cron/* and are scheduled in vercel.json. Core ones:

  • generate-club-invoices — daily 00:05 UTC. FIXED_MONTHLY and HYBRID clubs get invoiced on their billing day. Creates Stripe invoice + ClubInvoice DB record. Auto-charges saved card or emails Stripe-hosted invoice URL. Skips: suspended clubs, already-invoiced periods, zero-amount months.
  • apply-scheduled-changes — applies ScheduledChange records where effectiveDate ≤ now. (Note: the FEE_CHANGE applier moved to process-ending-classes per FEAT-154.)
  • auto-pay — processes auto-pay charges for class fees. Fires AutoPayPreviewEmail 3 days before.
  • execute-promotions — hourly. Runs coupon promotions.

Newer crons added with the 2026 feature wave include: auto-cancel-unpaid-enrollments (FEAT-125), reconcile-reg-fee-tracking (FEAT-149 safety net), monthly-club-reports + monthly-personal-reports (FEAT-144; now run daily and self-heal a missed day — BUG-241), reset-stale-streaks + award-perfect-attendance-month (FEAT-144), timesheet-reminders (FEAT-147), inventory-due-reminders (FEAT-166), sync-class-chat-membership (FEAT-163), evaluate-offline-abuse (FEAT-118), sync-stripe-disputes (FEAT-151), expire-coach-email-invites (FEAT-146), embed-stale-chunks (chatbot KB), plus attendance/session/event reminders and lane payment-hold expiry. Each is CRON_SECRET-protected and writes a CronLog row.

Invoice cron logic#

Daily at 00:05 UTC:

  • Finds FIXED_MONTHLY and HYBRID clubs whose billing day = today.
  • Creates Stripe invoice + ClubInvoice DB record.
  • Detects charge_automatically (if saved PaymentMethod or legacy default_source) vs send_invoice.
  • Skips: suspended clubs, already-invoiced periods, zero-amount months.

Webhook extensions#

Stripe webhook at /api/webhooks/stripe handles:

  • invoice.payment_succeeded → status: PAID, paidAt set.
  • invoice.payment_failed → status: OVERDUE, club owner notified. After 3 failures: admin escalation.
  • invoice.voided → status: VOID.

Two webhook endpoints: connected accounts (club payments) and platform account (platform subscriptions). Separate signing secrets.

Feature gating#

Per-club features gated via ClubFeatureGrant records. Common gates:

  • Lane/amenity booking system.
  • Coaching packages.
  • Equipment & inventory tracking — ASSET_TRACKING key, on by default for all clubs (admin can disable per club; FEAT-166).
  • Advanced analytics.
  • Tournament team builder (always on by default).

Modifying feature flags requires SUPER_ADMIN. Use @/lib/feature-gate helper for checking feature availability in routes and UI.

Chatbot knowledge base management#

Shipped (FEAT-108 → FEAT-116). The support chatbot and the public /help site both read from this knowledge base. Manage it at /dashboard/admin/config/chatbot. Backed by pgvector on Neon; embeddings via Voyage AI voyage-3-lite (512-dim); answer generation via Claude Haiku with the user's role + page context injected into the system prompt; retrieval is audience-filtered cosine similarity.

The two ways to update KB content#

The KB content is these very KBSection rows (this Admin Guide is one of them). There are two update paths:

  1. Git-canonical (the source of truth). The markdown lives in apps/web/src/content/kb/*.md (one file per audience/topic, with YAML frontmatter title / audience / covers). Edit those files in a PR, and after deploy run the seeder + embeddings:

    • cd apps/web && npm run seed:kb — upserts each KBSection and rebuilds its chunks from the files.
    • npm run embed:bootstrap — generates the pgvector embeddings (or just let the hourly embed-stale-chunks cron pick them up; any edited section has its lastEmbeddedAt nulled and is re-embedded automatically). This is the recommended path for large updates because the diff is reviewable and the repo stays authoritative.
  2. Admin UI (ad-hoc, no deploy). On /dashboard/admin/config/chatbot:

    • Sections tab — create or edit a section inline (slug, title, audience, status DRAFT/LIVE/ARCHIVED, covers, body). Saving a body change auto-re-embeds and snapshots a version. There's a "Preview chunks" view so you can see how the body will chunk.
    • Settings tab — bulk-upload a .md file or a .zip of markdown files (same frontmatter format; slug derived from filename; new slugs create, existing slugs update), plus a Full re-index button and the public kill switch.
    • Versions tab — per-section history with diff preview and revert (HIGH-tier confirm).
    • Analytics tab — question volume, resolution rate, top topics, by-role breakdown, and the unanswered questions review queue (dismiss / mark added-to-KB / mark added-as-chip).
    • Chips + IP blocks tabs — opening quick-reply chips and the abuse blocklist.

    Caution: UI-only edits drift from the git files, and the next seed:kb run overwrites them. For anything you want to keep, make the change in apps/web/src/content/kb/*.md (Path 1).

What to document#

The KB should describe what's live to end users, in user-facing language (not internal schema/flags). When a feature ships, fold its user-facing capability into the relevant audience file(s) and bump that file's last_updated + covers. The Analytics → unanswered-questions queue tells you what users are asking that the KB doesn't yet answer.

Guardrails (public surface)#

The unauthenticated/public chatbot has a 9-layer guardrail stack (rate limits, Turnstile CAPTCHA, IP blocklist, a monthly budget cap with auto-disable, and a manual kill switch via /api/admin/chatbot-kb/toggle-public). ChatbotBudgetState.publicDisabled is the single gate flag (budget-cap auto-trip and the manual kill switch both write it).

Anti-abuse measures#

Post flag-and-hide#

Any user flags a post → immediately auto-hidden from all feeds → admin reviews → restore or permanently remove. This is safer for a platform serving minors.

Rate limiting#

  • Notification table stores notification history. The in-app rate limit queries createdAt ≥ now - 60s and blocks non-exempt types beyond 5/user/60s. RATE_LIMIT_EXEMPT in notifications.ts lists event types that bypass.
  • Contact form: max 3 submissions per email per hour.

Role conflict validation#

System-wide role conflict prevention (as of V19):

  • COACH/CLUB_OWNER blocked from changing to PARENT/STUDENT.
  • PARENT/STUDENT blocked from changing to COACH/CLUB_OWNER.
  • Sole club owners blocked from any role change until the club has another owner.

Notification delivery status#

  • SMS (Twilio)live (FEAT-140 → FEAT-157 → FEAT-158). Dispatch routes through lib/sms-policy.ts:resolveSmsDispatch(), which consults the SMS kill switch, the per-(role, eventType) policy, and the user's smsLevel. REQUIRED-tier events override user opt-out. Admins manage all of it at /dashboard/admin/config/sms.
  • Web push (PWA)live (FEAT-142). VAPID web push to installed PWAs, plus the home-screen unread badge.
  • Native FCM / APN push — still console-log-only until the Firebase project is provisioned. The Expo push SDK is wired.

Standing rules (non-negotiable)#

From CLAUDE.md:

  • Every feature covers both web AND mobile (iOS + Android). No feature is done until both platforms ship. Implementation plans must include explicit mobile phases with mobile-specific file uploads listed.
  • Financial forward-only rule. No retroactive changes to existing payment records. New rates apply only to new payments.
  • Audit immutability. ClubActivityLog and ActionSnapshot entries are permanent. 2-year retention. 14-day admin revert window.
  • SMS (Twilio) and web push (PWA) are live; native FCM/APN push is still console-log-only. (The old "no SMS/push delivery" rule is retired.)
  • Two-step Prisma pattern. Always npx prisma db push --skip-generate then npx prisma generate.

Quick reference — where things live#

WhatWhere
Next feature number./bin/next-feat-number or project-state.jsonnextFeatureNumber
Next bug numberproject-state.jsonnextBugNumber
Current doc versionproject-state.jsoncurrentDocVersion
Feature plansdocs/FEAT-XXX-implementation-plan.md
Bug logdocs/bugs.md
Requirements docdocs/The_Cricket_Coach_business_plan_requirement_V20_merged_15Apr2026.md
Prisma schemaapps/web/prisma/schema.prisma (Neon PostgreSQL)
Prisma clientimport prisma from "@/lib/prisma" (singleton)
Auth resolutiongetAuthenticatedUser() from @/lib/auth-mobile (dual web/mobile)
Admin role checksisAdmin(user), isSuperAdmin(user) from @/lib/admin-auth
Activity loglogClubActivity() from @/lib/club-activity-log
Audit loglogAuditAction() from @/lib/admin-auth
NotificationscreateNotification(), sendNotification() from @/lib/notifications
Email templatesapps/web/src/emails/ (React Email + BaseTemplate.tsx)
Email deliveryResend (primary via @/lib/resend), Nodemailer Gmail SMTP (secondary), console (fallback)

Commit message standard#

Every commit must follow this format (enforced by .git/hooks/commit-msg):

<type>(FEAT-XXX): <summary under 72 chars>

Body (required):
- What was built or changed, in plain English
- Why — reference the phase from the implementation plan
- Any schema changes, new API routes, or new files created
- Any decisions made that aren't obvious from the diff

Phase: <phase number and name>
Affects: <comma-separated pages or API routes changed>

Types: feat, fix, refactor, test, docs, chore.

If this doesn't help#

For architectural questions, read CLAUDE.md first — it's the single source of truth, updated daily. For feature-specific questions, read docs/FEAT-XXX-implementation-plan.md. For bug context, read docs/bugs.md. For platform rules and locked decisions, read project-state.json.

If none of those answer your question, raise a ticket at https://prodemy.app/contact and tag it as admin-internal.

Was this article helpful?