AI-powered aviation exam preparation web app. Upload study materials, generate practice quizzes with AI, and track your progress.
- Framework: Next.js 16 (App Router, TypeScript)
- Styling: Tailwind CSS + shadcn/ui
- Auth & DB: Supabase (SSR via @supabase/ssr)
- AI: Groq (primary, Llama 4 Scout 17B multimodal) + Google Gemini 2.0 Flash (fallback)
- Payments: PayFast (via Supabase Edge Functions)
- Deployment: Vercel
- Domain: groundschoolai.site
src/
├── app/
│ ├── (app)/ # Authenticated routes (sidebar layout)
│ │ ├── dashboard/ # Document upload, storage, quiz generation
│ │ ├── quizzes/ # Quiz history list
│ │ ├── quiz/[id]/ # Take/review a quiz
│ │ ├── profile/ # User profile, account management
│ │ ├── captains-club/ # Subscription upgrade page
│ │ └── settings/ # App settings
│ │ └── admin/ # Admin dashboard (UUID-gated)
│ ├── api/
│ │ ├── admin/ # Admin analytics + actions API routes
│ │ └── generate-quiz/ # Server-side AI quiz generation
│ ├── auth/callback/ # Supabase auth callback
│ ├── login/ # Login/register page
│ └── layout.tsx # Root layout (dark mode, AuthProvider, Toaster)
├── components/
│ ├── layout/sidebar.tsx # Navigation sidebar
│ ├── providers/auth-provider.tsx # Auth context
│ └── ui/ # shadcn/ui components
├── lib/
│ ├── supabase/ # Supabase client (browser, server, middleware)
│ ├── constants.ts # Plan limits, formatBytes
│ ├── types.ts # TypeScript interfaces
│ └── utils.ts # cn() utility
middleware.ts # Auth route protection
supabase/ # Edge Functions (PayFast, account deletion)
- Middleware checks auth on every request
- Unauthenticated users →
/login - Authenticated users on auth pages →
/dashboard - Root
/→ redirects based on auth state - Supabase SSR handles cookies for session persistence
- API Route:
/api/generate-quiz(server-side, API keys never exposed) - Primary: Groq REST API (Llama 4 Scout 17B, multimodal — text + images)
- Fallback: Google Gemini 2.0 Flash (multimodal)
- Documents downloaded from Supabase Storage in parallel via
Promise.all - PDFs → text extraction via
unpdf; images → base64 for multimodal AI (capped at 4MB per image) - Text truncated to 30,000 chars per document
- Questions normalized to
{question_text, options[], correct_answer_id, explanation} - Rate limited: 1 request per 30 seconds per user (in-memory)
- Timeouts: 50-second AbortController on both AI providers
- Quota enforced server-side: basic plan = 5 quizzes/month, auto-resets monthly
- Edge Functions in
supabase/functions/handle payment data generation and ITN webhooks - Captain's Club subscription: R99/month
- Web: form POST redirect to PayFast
- Webhook updates
profiles.planin Supabase
| Table | Purpose |
|---|---|
| profiles | User profile, plan, storage, quotas |
| documents | Uploaded study materials metadata |
| quizzes | Generated quiz records |
| quiz_questions | Individual questions per quiz |
| quiz_attempts | User quiz attempt scores |
| Variable | Where | Purpose |
|---|---|---|
| NEXT_PUBLIC_SUPABASE_URL | Client + Server | Supabase project URL |
| NEXT_PUBLIC_SUPABASE_ANON_KEY | Client + Server | Supabase anon key |
| GROQ_API_KEY | Server only | Groq AI API key |
| GOOGLE_API_KEY | Server only | Gemini AI API key |
| NEXT_PUBLIC_AUTO_UPGRADE_NEW_USERS | Client | Auto-upgrade new users to Captain's Club |
| ADMIN_USER_ID | Server only | UUID of the admin user for dashboard access |
| SUPABASE_SERVICE_ROLE_KEY | Server only | Supabase service role key for admin dashboard (bypasses RLS) |
| Plan | Storage | Quizzes/month | Past Exams |
|---|---|---|---|
| Basic | 100MB | 5 | No |
| Captain's Club | 500MB | Unlimited | Yes |
-
2025-02-16: Content Strategy Alignment — Early Access Messaging + UX Improvements
- Onboarding modal (
onboarding-modal.tsx): Step 1 now shows "Two Ways to Study" — SACAA Question Bank first, then AI-Generated Exams. Step 3 replaced "Captain's Club" language with "Early Access" framing — "full early access" instead of "full premium access", lists features as "Your early access includes" instead of "Your Captain's Club includes", CTA changed to "Try the Question Bank or upload your notes". - Dashboard subtitle (
dashboard/page.tsx): Changed from "Upload study materials and generate AI-powered practice exams" to "Practice SACAA exam questions or generate AI exams from your notes" — leads with question bank. - Quizzes list scores (
quizzes/page.tsx): Completed exams now show score badges (green ≥75%, yellow <75%). Scores fetched fromquiz_attemptstable, latest attempt per quiz displayed. - Show Explanation button (
quiz/[id]/page.tsx): Added Lightbulb "Show Explanation" button that appears after selecting an answer during quiz-taking. Previously explanations were only visible in the post-submission review screen. - Captain's Club page (
captains-club/page.tsx): Now distinguishes between paid subscribers (pf_payment_idpresent) and early access users. Early access users see "Early Access — All Features Unlocked" with a note about future R99/month pricing. Paid users see the original "You're a Captain's Club Member!" message. - Sidebar (
sidebar.tsx): Plan label shows "Early Access" instead of "Captain's Club" for users withoutpf_payment_id. Badge shows "FREE" instead of "PRO" for early access users. - Profile per-subject scores (
profile/page.tsx): New "Score by Subject" card shows average scores per question bank subject with progress bars and exam counts. Parses subject from quiz title pattern"Subject - Practice Exam". Also fixed avg score calculation to exclude partial attempts (score = -1). - Decision: Question bank remains a paid plan (Captain's Club) feature. During early access, all users get CC for free.
- No DB migrations needed: All changes are UI/frontend only, using existing data.
- Onboarding modal (
-
2025-02-16: Finish Later + My Exams Tabs + Question Bank UI Redesign
- Finish Later: Added "Finish Later" button to quiz-taking screen. Saves current answers to
quiz_attemptswithscore = -1as a sentinel for in-progress. On resume, answers are restored from the partial attempt. On final submit, partial attempt is deleted and replaced with the real score, andquizzes.statusis set to"completed". - My Exams Tabs: Redesigned
/quizzespage with pill-style "In Progress" / "Completed" tabs. In Progress showsstatus = "active"quizzes with Resume (Play) button + amber Clock icon. Completed showsstatus = "completed"quizzes with Retake (RotateCcw) button + green CheckCircle icon. Each tab shows a count badge. Empty states are tab-aware. - Question Bank UI: Replaced the bottom "Start Exam Panel" with inline card expansion. Clicking a subject card toggles it open to reveal a
<select>dropdown for question count + Start button. Added 30 to question count options ([20, 30, 40, 60, 80]). ChevronDown/ChevronUp icons indicate expandable state. - No DB migrations needed: Uses existing
quizzes.statuscolumn ("active"vs"completed") andquiz_attempts.metadataJSON for storing partial answers. - Files changed:
src/app/(app)/quiz/[id]/page.tsx,src/app/(app)/quizzes/page.tsx,src/app/(app)/question-bank/page.tsx
- Finish Later: Added "Finish Later" button to quiz-taking screen. Saves current answers to
-
2025-02-14: Admin Dashboard — single-user UUID-gated admin panel
- Route:
/admininside(app)layout, shares sidebar with rest of app - Security: Triple-layer protection — (1) middleware redirects non-admin to
/dashboard, (2) API routes check UUID server-side, (3) page-level client check. Admin UUID hardcoded + env varADMIN_USER_ID - Sidebar: "Admin" nav item with Shield icon only renders when
user.id === ADMIN_UUID - Analytics: Users (total, active 7d/30d, plan breakdown, signups chart, top users), Exams (total, active, failed, stuck, attempts, avg score, pass rate, generation chart), Documents (total, storage, type breakdown, uploads chart), Revenue (MRR, paying users, conversion rate, cancelled-but-active), System Health (API key status, service role status, stuck quizzes)
- Actions: Upgrade/downgrade users, reset quotas, fix stuck quizzes, delete quizzes
- Auto-refresh: 60-second polling interval with toggle + manual refresh button
- API routes:
/api/admin/analytics(GET) and/api/admin/actions(POST), both UUID-gated server-side - Audit fixes (same day):
- CRITICAL: RLS bypass — Admin queries now use
SUPABASE_SERVICE_ROLE_KEYviasrc/lib/supabase/admin.tsto bypass RLS and see ALL users' data. Without this, every stat only showed the admin's own data. - Signups chart — Now uses
auth.admin.listUsers()to get realcreated_atfromauth.userstable. Previously usedprofiles.updated_atwhich changes on every profile update. - >1000 row handling — Added
fetchAll()pagination helper that loops with.range()to fetch beyond Supabase's default 1000-row limit. - Active users broadened — Now counts users who generated quizzes or uploaded documents, not just quiz attempts.
- Revenue accuracy — Active subscriptions now require
plan_status = 'active' OR 'trialing'. Addedcancelled_but_activecount for users whose subscription was cancelled but period hasn't ended. - delete_quiz cascade fix —
quiz_question_responsesare now deleted viaattempt_id(correct FK), notquestion_id. - Graceful fallback — If
SUPABASE_SERVICE_ROLE_KEYis missing, dashboard falls back to regular client (shows only admin's data) with a prominent warning banner instead of crashing.
- CRITICAL: RLS bypass — Admin queries now use
- Route:
-
2025-02-13: Security & reliability audit — all 20 items (P0–P3) resolved
- Upload hardening: 25MB per-file limit, .doc/.docx rejected, orphaned storage files cleaned up on DB failure
- Upload progress: Real byte-level progress via XHR to Supabase Storage REST API
- Server-side upload validation: New
/api/validate-uploadroute checks quota, file size, and extension before upload - Delete safety: DB record deleted first, then storage (prevents ghost records)
- Quiz quota enforcement: Server-side check of
monthly_quizzes_remainingin/api/generate-quiz, auto-resets monthly - Rate limiting: In-memory 30s cooldown per user on quiz generation
- Auth hardening:
userIdderived from session, never from request body - AI timeouts: 50s AbortController on Groq and Gemini calls
- Image processing: Images resized to 1024px max via
sharp, converted to JPEG 80% quality before AI - Parallel fetching: Documents fetched concurrently via
Promise.allinstead of sequential loop - N+1 elimination: Quiz title built from already-fetched content, no extra DB queries
- Safe quiz insert: Quiz created with
generatingstatus, set toactiveonly after questions succeed - Error handling: Quiz attempt save warns user on failure; profile creation failure returns error
- Structured logging:
[quiz-gen]prefixed logs at every decision point for Vercel Logs - Pagination: Documents and quizzes lists show 20 at a time with "Show More"
- Quizzes filter: Only
activequizzes shown in list (hides failedgeneratingrecords) - See:
AUDIT.mdfor full audit findings
-
2025-02-09: Complete rebuild from React Native/Expo to pure Next.js web app
- Reason: iOS Safari white screen caused by incompatible React Native web libraries (navigator.locks, react-native-reanimated, react-native-svg). Multiple fix attempts failed. Clean Next.js build eliminates all RN-web compatibility issues.
- Preserved: Same Supabase project, same Edge Functions, same PayFast integration, same domain
- Removed: PostHog analytics (per user request), React Native, Expo, Metro bundler
- Added: Next.js 16, Tailwind CSS, shadcn/ui, @supabase/ssr, middleware auth