diff --git a/CLAUDE.md b/CLAUDE.md index 733e56a1..24194f1c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -116,17 +116,18 @@ All routes live under `app/[locale]/` (`[locale]` is always `/en/` or `/es/`). P ### Database Schema Essentials -65+ tables. Key groups: multi-tenancy (`tenants`, `tenant_users`, `tenant_settings`, `super_admins`) · users (`profiles` — global, no tenant_id) · content (`courses`, `lessons`, `exercises`, `exams`, `exam_questions`) · progress (`enrollments` — progress only, `lesson_completions`, `exam_submissions`) · commerce (`products`, `plans`, `transactions`, `subscriptions`, `payment_requests`, `entitlements` — course-access source of truth) · revenue (`revenue_splits`, `payouts`, `invoices`) · platform billing (`platform_plans`, `platform_subscriptions`, `platform_payment_requests`) · gamification (12 tables incl. `gamification_profiles`, `xp_transactions`, `achievements`, `challenges`) · certificates · notifications. +116 tables. Key groups: multi-tenancy (`tenants`, `tenant_users`, `tenant_settings`, `super_admins`) · users (`profiles` — global, no tenant_id) · content (`courses`, `lessons`, `exercises`, `exams`, `exam_questions`) · progress (`enrollments` — progress only, `lesson_completions`, `exam_submissions`) · commerce (`products`, `plans`, `transactions`, `subscriptions`, `payment_requests`, `entitlements` — course-access source of truth) · revenue (`revenue_splits`, `payouts`, `invoices`) · platform billing (`platform_plans`, `platform_subscriptions`, `platform_payment_requests`) · gamification (10 `gamification_*` tables incl. `gamification_profiles`, `gamification_xp_transactions`, `gamification_achievements`; plus leagues in `league_tiers`/`league_memberships`) · certificates · notifications. `profiles` and `gamification_levels` are global (no `tenant_id`). **Key RPCs:** ```typescript supabase.rpc('enroll_user', { _user_id, _product_id }) -supabase.rpc('handle_new_subscription', { _user_id, _plan_id, _transaction_id }) // trigger-invoked; writes to entitlements -supabase.rpc('award_xp', { p_user_id, p_action_type, p_reference_id }) +supabase.rpc('handle_new_subscription', { _user_id, _plan_id, _transaction_id, _start_date }) // trigger-invoked; writes to entitlements +supabase.rpc('has_course_access', { _user_id, _course_id }) // access check; _course_id is integer — cast ::int from SQL +supabase.rpc('award_xp', { _user_id, _action_type, _xp_amount, _reference_id, _reference_type }) // overload adds _tenant_id supabase.rpc('create_exam_submission', { p_student_id, p_exam_id, p_answers }) -supabase.rpc('save_exam_feedback', { submission_id, exam_id, student_id, answers, overall_feedback, score }) +supabase.rpc('save_exam_feedback', { p_submission_id, p_exam_id, p_student_id, p_answers, p_overall_feedback, p_score, p_question_feedback, p_ai_model, p_processing_time_ms }) supabase.rpc('get_plan_features', { _tenant_id }) ``` @@ -151,16 +152,14 @@ NEXT_PUBLIC_PLATFORM_DOMAIN= # e.g. lvh.me for local dev, lmsplatform.com ## Testing -E2E tests in `tests/playwright/`, four files by priority: -- `multi-tenant-isolation.spec.ts` — P0, 8 tests -- `authentication-security.spec.ts` — P0, 6 tests -- `payment-security.spec.ts` — P0, 7 tests -- `comprehensive-security-audit.spec.ts` — P1/P2, 26 tests +E2E tests in `tests/playwright/` — 33 spec files (tenant isolation, auth security, payment flows, enrollment, entitlements, gamification, community, i18n, platform panel, plan change, teacher/admin CRUD). Read the directory rather than a list here; it changes often. Highest-priority: `tenant-isolation.spec.ts`, `auth-security.spec.ts`, `payment-flows.spec.ts`, `evaluations-security.spec.ts`. + +The Playwright config has no `webServer` — start `npm run dev` yourself first, use `lvh.me` (never `localhost`), and keep `--workers=1` locally or GoTrue rate-limits the sign-ins. Unit tests: `npm run test:unit` (Vitest, `tests/unit/`). Test accounts (from `supabase/seed.sql`, seeded by `supabase db reset`): - `student@e2etest.com` / `password123` — student (Default School) -- `owner@e2etest.com` / `password123` — admin (Default School) -- `creator@codeacademy.com` / `password123` — teacher (Code Academy, subdomain `code-academy.lvh.me:3000`) +- `owner@e2etest.com` / `password123` — admin (Default School) **+ super admin** (`/platform/*`) +- `creator@codeacademy.com` / `password123` — **admin** (Code Academy, subdomain `code-academy.lvh.me:3000`) - `alice@student.com` / `password123` — student (Code Academy) Pre-commit checklist: `npm run build` · tenant filter on every query · tested with all relevant roles · loading + error states handled. @@ -174,7 +173,11 @@ Pre-commit checklist: `npm run build` · tenant filter on every query · tested - **`profiles` has no `email`** — get emails via `createAdminClient().auth.admin.getUserById()`. - **`exam_submissions` order column** is `submission_date`, not `submitted_at`. - **Transaction status** is `'successful'`, not `'succeeded'`. -- Exam child tables (`exam_questions`, `exam_answers`, `exam_question_scores`, `exam_scores`) have **no `tenant_id`** — filter by exam_id/submission_id only; adding `.eq('tenant_id', …)` errors the whole query. +- Exam child tables (`exam_questions`, `exam_answers`, `exam_question_scores`, `exam_scores`, `question_options`) have **no `tenant_id`** — filter by exam_id/submission_id only; adding `.eq('tenant_id', …)` errors the whole query. `exam_questions` also has **no `sequence`**. +- **`exercise_completions` uses `user_id`**, not `student_id`, and stores only `score` + `completed_at` — submissions/feedback live in `exercise_evaluations` and the `exercise_*_submissions` tables. +- **`subscriptions` uses `subscription_status`** (not `status`), `provider_subscription_id` (not `stripe_subscription_id`), and `created` (not `created_at`). +- **`plans` uses `plan_name` and `duration_in_days`** — there is no `name`, no `interval`, no `status`. Soft-deleted via `deleted_at`. +- **`transactions` has three unique indexes**, product-shaped and plan-shaped kept separate plus `(payment_provider, provider_charge_id)` for webhook idempotency — see `docs/DATABASE_SCHEMA.md`. - **Creating test users via SQL** won't fire `handle_new_user()` — manually insert `profiles`, `user_roles`, `auth.identities`. Use `NULL` for `phone`, `''` for nullable strings. - **`proxy.ts` is the only middleware** — do not create `middleware.ts` (conflict). - **`createAdminClient()`** lives in `@/lib/supabase/admin`, NOT `@/lib/supabase/server`. diff --git a/docs/ACTUAL_SCHEMA.md b/docs/ACTUAL_SCHEMA.md index ee9595cc..5f8d6a81 100644 --- a/docs/ACTUAL_SCHEMA.md +++ b/docs/ACTUAL_SCHEMA.md @@ -1,6 +1,17 @@ # Actual Database Schema Reference -This file contains the **actual** column names from the live database (discovered 2026-02-08). +> ## ⚠️ OUT OF DATE — do not trust this file +> +> This was a snapshot of the live database taken **2026-02-08**. The schema has changed since; parts of this page are now wrong. Known example: the `enrollments` section below still lists `product_id` / `subscription_id` and their CHECK constraint, all of which were **dropped** when access moved to the `entitlements` table (migration `20260516150000`). +> +> Current sources of truth, in order: +> 1. **`lib/database.types.ts`** — generated from the database, always correct +> 2. **The live database** — see [DATABASE_SCHEMA.md § Verifying this document](./DATABASE_SCHEMA.md#verifying-this-document) +> 3. **[DATABASE_SCHEMA.md](./DATABASE_SCHEMA.md)** — curated and maintained +> +> Kept only as a historical record of what the schema looked like in February 2026. + +This file contains the column names discovered in the live database on 2026-02-08. ## Core Tables diff --git a/docs/AI_AGENT_GUIDE.md b/docs/AI_AGENT_GUIDE.md index 2be018bc..1c55b319 100644 --- a/docs/AI_AGENT_GUIDE.md +++ b/docs/AI_AGENT_GUIDE.md @@ -302,26 +302,32 @@ lessons, exercises, exams (tenant-scoped) ### Key Functions to Know -1. **`enroll_user(_user_id, _product_id)`** - - Enrolls user in ALL courses linked to product (loops through `product_courses`) - - Sets `status = 'active'` and inherits `tenant_id` from product +Signatures below are the real ones — verify with `pg_get_function_identity_arguments` before calling (see [DATABASE_SCHEMA.md § Verifying this document](./DATABASE_SCHEMA.md#verifying-this-document)). + +1. **`enroll_user(_user_id uuid, _product_id integer)`** + - Grants access to ALL courses linked to the product (loops through `product_courses`) + - Writes to **`entitlements`** — the source of truth for access — inheriting `tenant_id` from the product - Called automatically on successful payment -2. **`get_plan_features(_tenant_id)`** +2. **`has_course_access(_user_id uuid, _course_id integer)`** + - The access check. Second arg is `integer` — cast `::int` from SQL + - No staff branch: teachers/admins are not implicitly granted access here + +3. **`get_plan_features(_tenant_id uuid)`** - Returns plan info, features (JSONB), and limits for the tenant - Single source of truth for feature gating - `SECURITY DEFINER` — works regardless of caller's RLS context -3. **`create_exam_submission(student_id, exam_id, answers)`** - - Creates exam submission - - Returns submission ID +4. **`create_exam_submission(p_student_id uuid, p_exam_id integer, p_answers jsonb)`** + - Creates exam submission, returns `submission_id` -4. **`save_exam_feedback(submission_id, ...)`** - - Saves AI feedback to exam - - Updates score and marks as reviewed +5. **`save_exam_feedback(p_submission_id, p_exam_id, p_student_id, p_answers, p_overall_feedback, p_score, p_question_feedback, p_ai_model, p_processing_time_ms)`** + - Saves AI feedback to the exam and updates the score + - Nine params, all `p_`-prefixed -5. **`award_xp(p_user_id, p_action_type, p_reference_id)`** - - Awards XP for gamification actions +6. **`award_xp(_user_id uuid, _action_type text, _xp_amount integer, _reference_id text, _reference_type text)`** + - Awards XP for gamification actions; creates the gamification profile lazily + - An overload takes a trailing `_tenant_id uuid` — trigger functions call that one ### Querying with Relations @@ -377,7 +383,7 @@ If query returns empty when it shouldn't: 4. Verify user has required enrollment/role 5. Test with `createAdminClient()` to bypass RLS (temporarily, for debugging only) -**RLS is enabled on ALL tenant-scoped tables** (65+ tables). Standard policy pattern: +**RLS is enabled on ALL tenant-scoped tables** (116 tables in `public`, 61 of them carrying a `tenant_id`). Standard policy pattern: - SELECT: users who are members of the tenant (checked via `tenant_users`) - INSERT/UPDATE/DELETE: users with `teacher` or `admin` role in the tenant - Special cases: students can INSERT own `enrollments`, `lesson_completions`, `exam_submissions` diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md index 5706d0e5..e22e5a17 100644 --- a/docs/DATABASE_SCHEMA.md +++ b/docs/DATABASE_SCHEMA.md @@ -2,37 +2,50 @@ ## Overview -The LMS database is built on PostgreSQL 15 via Supabase. It consists of **65+ tables** organized into logical domains: - -- **User Management**: `profiles` (global), `user_roles`, `roles`, `permissions` -- **Multi-Tenancy**: `tenants`, `tenant_users`, `tenant_settings`, `tenant_invitations`, `super_admins` -- **Course Content**: `courses`, `lessons`, `exercises`, `exams`, `exam_questions`, `question_options`, `lesson_resources`, `lessons_ai_tasks`, `lessons_ai_task_messages`, `prompt_templates` -- **Enrollment & Progress**: `enrollments`, `subscriptions`, `lesson_completions`, `exam_submissions`, `exercise_completions` -- **Commerce**: `products`, `product_courses`, `plans`, `transactions`, `payment_requests` -- **Platform Billing**: `platform_plans`, `platform_subscriptions`, `platform_payment_requests` +The LMS database is built on PostgreSQL 15 via Supabase. As of the latest migration it has **116 tables** in the `public` schema, organized into logical domains: + +- **User Management**: `profiles` (global), `user_roles`, `roles`, `permissions`, `role_permissions` +- **Multi-Tenancy**: `tenants`, `tenant_users`, `tenant_settings`, `tenant_invitations`, `super_admins`, `system_settings` +- **Course Content**: `courses`, `course_categories`, `lessons`, `lesson_resources`, `lesson_checkpoints`, `lesson_checkpoint_attempts`, `exercises`, `exams`, `exam_questions`, `question_options`, `content_versions`, `lessons_ai_tasks`, `lessons_ai_task_messages`, `prompt_templates` +- **Access Control**: **`entitlements`** — the source of truth for course access +- **Progress**: `enrollments` (progress record only), `lesson_completions`, `lesson_passed`, `lesson_views`, `exam_submissions`, `exam_answers`, `exam_scores`, `exam_question_scores`, `exercise_completions`, `exercise_evaluations` +- **Commerce**: `products`, `product_courses`, `product_post_registration_steps`, `plans`, `plan_courses`, `transactions`, `subscriptions`, `payment_requests`, `webhook_events`, `tenant_payment_wallets` +- **Platform Billing**: `platform_plans`, `platform_subscriptions`, `platform_payment_requests`, `access_cutoff_notifications` - **Revenue**: `revenue_splits`, `payouts`, `invoices` -- **Gamification**: 12 tables (`gamification_profiles`, `gamification_xp_transactions`, `gamification_levels`, `gamification_achievements`, `gamification_user_achievements`, `gamification_store_items`, `gamification_redemptions`, `gamification_user_rewards`, `gamification_leaderboard_cache`, `gamification_challenge_participants`, `gamification_daily_caps`, `gamification_challenges`) -- **Certificates**: `certificates`, `certificate_templates` -- **AI Tutoring**: `course_ai_tutors`, `aristotle_sessions`, `aristotle_messages` +- **Gamification**: 10 `gamification_*` tables plus leagues (`league_tiers`, `league_memberships`) +- **Adaptive Practice (FSRS/Elo)**: `review_cards`, `practice_attempts`, `student_topic_ratings`, `study_goals` +- **Certificates**: `certificates`, `certificate_templates`, `certificate_shares`, `certificate_verification_log`, `issuer_keys` +- **AI Tutoring**: `course_ai_tutors`, `aristotle_sessions`, `aristotle_messages`, `exam_ai_configs` - **Landing Pages**: `landing_pages`, `landing_page_templates` -- **Social**: `messages`, `chats`, `lesson_comments`, `reviews` +- **Community**: `community_posts`, `community_comments`, `community_reactions`, `community_poll_options`, `community_poll_votes`, `community_flags`, `community_user_mutes` +- **Social / Messaging**: `messages`, `chats`, `chat_conversations`, `chat_messages`, `lesson_comments`, `comments`, `comment_reactions`, `comment_flags`, `reviews`, `item_ratings` - **Support**: `tickets`, `ticket_messages` -- **Notifications**: `notifications`, `user_notifications`, `notification_templates`, `notification_preferences` -- **API/MCP**: `mcp_api_tokens` -- **Media**: `exercise_media_submissions` +- **Notifications**: `notifications`, `user_notifications`, `notification_templates`, `notification_preferences`, `device_push_tokens` +- **API/MCP**: `mcp_api_tokens`, `mcp_audit_log` +- **Media**: `exercise_media_submissions`, `exercise_files`, `exercise_code_student_submissions` +- **Legacy / unused**: `assignments`, `submissions`, `grades` — present but not wired into current flows + +For the exact current list, don't trust this page — ask the database (see [Verifying this document](#verifying-this-document)). ### Conventions & Pitfalls -- **Primary keys use `_id` suffix**: `product_id`, `course_id`, `enrollment_id`, `transaction_id`, `submission_id`, `request_id`, `exam_id`, `question_id`, `option_id`, `score_id` -- **`profiles` is GLOBAL** — no `tenant_id` column. Same for `gamification_levels`. -- **`lesson_completions` uses `user_id`** (NOT `student_id`) -- **`exam_submissions` order column is `submission_date`** (NOT `submitted_at`) +- **Primary keys use `_id` suffix**: `product_id`, `course_id`, `enrollment_id`, `transaction_id`, `submission_id`, `request_id`, `exam_id`, `question_id`, `option_id`, `score_id`. Exceptions: `lessons`, `exercises` and `lesson_completions` use a bare `id`. +- **Access lives in `entitlements`, not `enrollments`** (since migration `20260516150000`). `enrollments` is a learning-progress record only; its `product_id`/`subscription_id` columns and their CHECK constraint were **dropped**. +- **`profiles` is GLOBAL** — no `tenant_id`. Same for `gamification_levels`, `platform_plans`, `plan_courses`, and ~50 child tables (full list under [Tables without `tenant_id`](#tables-without-tenant_id)). Adding `.eq('tenant_id', …)` to a table that lacks the column **errors the entire query**. +- **`lesson_completions` uses `user_id`** (NOT `student_id`), and has no `tenant_id`. +- **`exercise_completions` uses `user_id`** (NOT `student_id`), and has no `tenant_id`. +- **`exam_submissions` uses `student_id`**, and its order column is `submission_date` (NOT `submitted_at`). +- **Exam child tables have no `tenant_id`**: `exam_questions`, `exam_answers`, `exam_scores`, `exam_question_scores`, `question_options`. Isolation comes from RLS through the parent exam/submission. +- **`exams` has no `passing_score` and no `allow_retake`** — use 70 as the default threshold and assume retakes are allowed. +- **`exam_questions` has no `sequence` column** — questions come back in insertion order. - **Transaction status is `'successful'`** (NOT `'succeeded'`). Full enum: `pending`, `successful`, `failed`, `archived`, `canceled`, `refunded` - **`product_courses` can have multiple rows per course** — a single course can belong to many products. **NEVER use `.single()`** on this table. -- **Enrollment status** uses enum `enrollement_status`: `'active'` | `'disabled'` (note the typo in the enum name is intentional/legacy) -- **Enrollments CHECK constraint**: requires either `product_id` OR `subscription_id` (not both, not neither) +- **Enrollment status** uses enum `enrollement_status`: `'active'` | `'disabled'` (the typo in the enum name is legacy — keep it) +- **`subscriptions` status column is `subscription_status`**, not `status`; the provider id is `provider_subscription_id`, not `stripe_subscription_id`. - **`certificates` has TWO FKs to `profiles`** (`user_id`, `revoked_by`) — must use FK hint: `profiles!certificates_user_id_fkey(...)` - **`profiles` has NO `email` column** — get emails via `createAdminClient().auth.admin.getUserById()` +- **`courses` has no `slug`** — route by `course_id`. +- **`gamification_profiles` has no `coins` column** — balance = `floor(total_xp / 10) - total_coins_spent`. --- @@ -47,10 +60,18 @@ The LMS database is built on PostgreSQL 15 via Supabase. It consists of **65+ ta |--------|------|-------| | `id` | UUID PK | References `auth.users(id)` | | `full_name` | TEXT | | +| `username` | TEXT | | | `avatar_url` | TEXT | | +| `website` | TEXT | | | `bio` | TEXT | | +| `currency_id` | | | +| `stripe_customer_id` | TEXT | Student payments (Connect). Note a legacy `stripeCustomerID` column also exists | +| `data_person` | JSONB | | +| `onboarding_completed` | BOOLEAN | | +| `deactivated_at` | TIMESTAMPTZ | | | `created_at` | TIMESTAMPTZ | | -| `updated_at` | TIMESTAMPTZ | | + +**No `email` column** and **no `updated_at`**. Emails come from `createAdminClient().auth.admin.getUserById()`. #### `user_roles` Assigns global roles to users. Default role `student` assigned on signup. @@ -87,6 +108,8 @@ School/organization records. | `billing_email` | VARCHAR(255) | | | `billing_period_end` | TIMESTAMPTZ | | | `billing_status` | VARCHAR(50) | `free`, `active`, `past_due`, `canceled` | +| `access_cutoff_at` | TIMESTAMPTZ | When an unpaid school's content access is suspended | +| `stripe_charges_enabled` / `stripe_payouts_enabled` / `stripe_details_submitted` | BOOLEAN | Stripe Connect onboarding state | #### `tenant_users` Many-to-many user-tenant relationships. **Authoritative source for per-tenant roles.** @@ -157,10 +180,13 @@ Course catalog. Tenant-scoped. | `category_id` | INTEGER FK → course_categories | | | `status` | VARCHAR(50) | `draft`, `published`, `archived` | | `require_sequential_completion` | BOOLEAN | Default `false` | -| `created_at` | TIMESTAMPTZ | | -| `updated_at` | TIMESTAMPTZ | | +| `learning_objectives` | TEXT[] | **`text[]`, not JSONB** | +| `estimated_duration_minutes` | INTEGER | | +| `tags` | | | +| `published_at` / `archived_at` / `deleted_at` | TIMESTAMPTZ | Soft lifecycle — filter `deleted_at IS NULL` | +| `created_at` / `updated_at` | TIMESTAMPTZ | | -No `slug` column — use `course_id` for routing. +No `slug` column — use `course_id` for routing. `author_id` has **two** FKs (`courses_author_id_fkey` → `auth.users`, `courses_author_profile_fkey` → `profiles`), so joins to `profiles` need an explicit FK hint. #### `course_categories` Course categorization. Tenant-scoped. @@ -190,8 +216,14 @@ Course lessons. Tenant-scoped. | `sequence` | INTEGER | Order within course | | `status` | VARCHAR(50) | `draft`, `published` | | `publish_at` | TIMESTAMPTZ | Scheduled publishing | -| `created_at` | TIMESTAMPTZ | | -| `updated_at` | TIMESTAMPTZ | | +| `is_preview` | BOOLEAN | Viewable without access (public preview) | +| `description` / `summary` / `image` | | | +| `embed_code` | TEXT | Alternative to `video_url` | +| `transcript` | TEXT | Video transcript (YouTube captions) | +| `ai_task_description` / `ai_task_instructions` | TEXT | Inline AI task; richer config lives in `lessons_ai_tasks` | +| `created_at` / `updated_at` | TIMESTAMPTZ | | + +PK is a bare `id`, **not** `lesson_id` — XP triggers joining `lessons` on `l.lesson_id` is a known past bug. `UNIQUE(course_id, sequence)` @@ -254,29 +286,34 @@ Practice exercises within lessons. Tenant-scoped. | `description` | TEXT | | | `exercise_type` | `exercise_type` enum | See values below | | `exercise_config` | JSONB | Type-specific configuration | -| `initial_code` | TEXT | For code exercises | -| `solution_code` | TEXT | | -| `test_cases` | JSONB | | +| `instructions` | TEXT | Student-facing prompt | +| `system_prompt` | TEXT | For AI-evaluated exercises | +| `difficulty_level` | `difficulty_level` | `easy`, `medium`, `hard` | +| `time_limit` | INTEGER | | +| `course_id` | INTEGER FK → courses | Denormalized alongside `lesson_id` | +| `active_file` / `visible_files` | | Sandpack code-exercise setup | +| `template_id` / `template_variables` | | Built from a `prompt_templates` row | +| `created_by` | UUID | | | `status` | VARCHAR(50) | | -| `created_at` | TIMESTAMPTZ | | -| `updated_at` | TIMESTAMPTZ | | +| `created_at` / `updated_at` | TIMESTAMPTZ | | + +There are no `initial_code`, `solution_code` or `test_cases` columns — code-exercise setup lives in `exercise_config` / `active_file` / `visible_files`. **`exercise_type` enum values:** `quiz`, `coding_challenge`, `essay`, `multiple_choice`, `true_false`, `fill_in_the_blank`, `discussion`, `audio_evaluation`, `video_evaluation`, `real_time_conversation`, `artifact` #### `exercise_completions` -Student exercise submissions. +Records that a student finished an exercise. **Uses `user_id`, and has NO `tenant_id`** — filtering by `tenant_id` errors the query. | Column | Type | Notes | |--------|------|-------| | `id` | SERIAL PK | | | `exercise_id` | INTEGER FK → exercises | | -| `student_id` | UUID FK → profiles | | -| `submission` | TEXT | | -| `is_correct` | BOOLEAN | | -| `ai_feedback` | TEXT | | +| `user_id` | UUID FK → auth.users | **NOT `student_id`** | +| `completed_by` | UUID | Who marked it complete (self or teacher) | +| `score` | NUMERIC | | | `completed_at` | TIMESTAMPTZ | | -`UNIQUE(exercise_id, student_id)` +The submission content and AI feedback live in `exercise_evaluations` / `exercise_code_student_submissions` / `exercise_media_submissions`, not here. #### `exercise_messages` AI chat for exercise help. @@ -329,6 +366,7 @@ Course assessments. Tenant-scoped. | `updated_at` | TIMESTAMPTZ | | #### `exam_questions` +**No `tenant_id`, no `sequence`, no `created_at`.** Filter by `exam_id` only — isolation comes from RLS on the parent exam. | Column | Type | Notes | |--------|------|-------| @@ -337,8 +375,13 @@ Course assessments. Tenant-scoped. | `question_text` | TEXT | | | `question_type` | VARCHAR(50) | `multiple_choice`, `true_false`, `free_text` | | `points` | INTEGER | Default 1 | -| `sequence` | INTEGER | | -| `created_at` | TIMESTAMPTZ | | +| `correct_answer` | TEXT | | +| `ai_grading_criteria` | TEXT | Guidance for AI grading | +| `grading_rubric` | JSONB | | +| `expected_keywords` | TEXT[] | | +| `max_length` | INTEGER | For free-text answers | + +Sibling child tables — also **without `tenant_id`**: `question_options`, `exam_answers`, `exam_scores`, `exam_question_scores`. #### `question_options` Options for multiple choice/true-false questions. @@ -363,8 +406,11 @@ Student exam submissions. **Order column is `submission_date`** (NOT `submitted_ | `submission_date` | TIMESTAMPTZ | **NOT `submitted_at`** | | `ai_data` | JSONB | AI feedback per question | | `score` | NUMERIC | | +| `feedback` | TEXT | Overall feedback | | `evaluated_at` | TIMESTAMPTZ | | -| `is_reviewed` | BOOLEAN | | +| `review_status` | VARCHAR | **NOT `is_reviewed`** | +| `requires_attention` | BOOLEAN | Flags low-confidence AI grades for a human | +| `ai_model_used` / `ai_processing_time_ms` / `ai_confidence_score` | | Grading telemetry | Processed by `create_exam_submission()` and `save_exam_feedback()` RPCs. @@ -372,23 +418,43 @@ Processed by `create_exam_submission()` and `save_exam_feedback()` RPCs. ### Enrollment & Commerce +#### `entitlements` +**The source of truth for course access.** Polymorphic: one row per (user, course, source). Tenant-scoped. Introduced by migration `20260516150000`. + +| Column | Type | Notes | +|--------|------|-------| +| `entitlement_id` | BIGINT PK | GENERATED ALWAYS AS IDENTITY | +| `user_id` | UUID FK → auth.users | NOT NULL | +| `course_id` | INTEGER FK → courses(course_id) | NOT NULL | +| `tenant_id` | UUID FK → tenants | NOT NULL | +| `source_type` | `entitlement_source` | `product`, `subscription`, `free`, `admin_grant` | +| `source_id` | INTEGER | The product/plan id — NULL for `free` and `admin_grant` | +| `status` | `entitlement_status` | `active`, `revoked`, `expired` (there is **no** `canceled`) | +| `granted_at` | TIMESTAMPTZ | NOT NULL, default `now()` | +| `expires_at` | TIMESTAMPTZ | NULL = no expiry | +| `revoked_at` | TIMESTAMPTZ | | + +`UNIQUE(user_id, course_id, source_type, source_id) NULLS NOT DISTINCT` — so a user can hold access to the same course from a product *and* a subscription simultaneously without collision. + +**CHECK `entitlements_source_id_shape`**: `source_id` must be NOT NULL for `product`/`subscription`, and NULL for `free`/`admin_grant`. + +Written by `enroll_user()` and `handle_new_subscription()`. Read via `has_course_access(_user_id uuid, _course_id integer)` — note the second arg is `integer`, so cast `::int` when calling from SQL. + #### `enrollments` -Student course enrollments. Tenant-scoped. +**Learning-progress record only** — it does *not* grant access. Tenant-scoped. | Column | Type | Notes | |--------|------|-------| | `enrollment_id` | INTEGER PK | | +| `user_id` | UUID FK → auth.users | NOT NULL | +| `course_id` | INTEGER FK → courses(course_id) | NOT NULL | | `tenant_id` | UUID FK → tenants | NOT NULL | -| `user_id` | UUID FK → profiles | | -| `course_id` | INTEGER FK → courses(course_id) | | -| `product_id` | INTEGER FK → products | | -| `subscription_id` | INTEGER FK → subscriptions | | | `status` | `enrollement_status` | `active`, `disabled` | | `enrollment_date` | TIMESTAMPTZ | | -**CHECK constraint**: `(product_id IS NOT NULL AND subscription_id IS NULL) OR (product_id IS NULL AND subscription_id IS NOT NULL)` +`UNIQUE(user_id, course_id)` -Auto-created by `enroll_user()` RPC. +> The old `product_id` / `subscription_id` columns and their "one or the other" CHECK constraint were **dropped** in the entitlements migration. If you find code or docs referencing them, they are out of date. Students on a subscription self-enroll from `/dashboard/student/browse` (`useEnrollment()` hook); subscriptions grant access but do not auto-enroll. #### `products` Purchasable course bundles. Tenant-scoped. @@ -400,9 +466,11 @@ Purchasable course bundles. Tenant-scoped. | `name` | VARCHAR(255) | | | `description` | TEXT | | | `price` | NUMERIC | | -| `stripe_product_id` | TEXT | | -| `stripe_price_id` | TEXT | | -| `payment_provider` | VARCHAR(50) | `stripe`, `manual`, `paypal` | +| `currency` | `currency_type` | `usd`, `eur`, `mxn`, `cop`, `clp`, `pen`, `ars`, `brl` | +| `image` | TEXT | | +| `payment_provider` | VARCHAR(50) | `stripe`, `manual`, `paypal`, `lemonsqueezy`, `solana`, `solana_subs`, `binance`, `binance_personal` | +| `provider_product_id` | TEXT | **NOT `stripe_product_id`** — provider-agnostic | +| `provider_price_id` | TEXT | **NOT `stripe_price_id`** | | `status` | VARCHAR(50) | | | `created_at` | TIMESTAMPTZ | | @@ -419,35 +487,50 @@ Links products to courses. Tenant-scoped. **A course can belong to multiple prod `UNIQUE(product_id, course_id)` #### `plans` -Subscription plans (school-level). Tenant-scoped. +Subscription plans sold by a school to its students. Tenant-scoped. **Column names differ from `products` — there is no `name`, no `interval`, no `status`.** | Column | Type | Notes | |--------|------|-------| | `plan_id` | SERIAL PK | | | `tenant_id` | UUID FK → tenants | NOT NULL | -| `name` | VARCHAR(255) | | +| `plan_name` | VARCHAR(100) | **NOT `name`** | | `description` | TEXT | | -| `price` | NUMERIC | | -| `interval` | VARCHAR(50) | `month`, `year` | -| `stripe_product_id` | TEXT | | -| `stripe_price_id` | TEXT | | -| `status` | VARCHAR(50) | | +| `price` | NUMERIC(10,2) | NOT NULL | +| `duration_in_days` | INTEGER | **NOT `interval`** — CHECK > 0 (e.g. 30, 365) | +| `currency` | `currency_type` | `usd`, `eur`, `mxn`, `cop`, `clp`, `pen`, `ars`, `brl` | +| `features` | TEXT | | +| `thumbnail` | TEXT | | +| `payment_provider` | VARCHAR(20) | `stripe`, `manual`, `paypal`, `lemonsqueezy`, `solana`, `solana_subs`, `binance`, `binance_personal` | +| `provider_product_id` | TEXT | Provider-agnostic (**not** `stripe_product_id`) | +| `provider_price_id` | TEXT | | +| `deleted_at` | TIMESTAMPTZ | Soft delete — filter it out | | `created_at` | TIMESTAMPTZ | | +#### `plan_courses` +Which courses a plan covers. **No `tenant_id`** — scope through the parent plan. + #### `subscriptions` -User subscriptions to plans. Tenant-scoped. +Student subscriptions to a school's plans. Tenant-scoped. | Column | Type | Notes | |--------|------|-------| | `subscription_id` | SERIAL PK | | | `tenant_id` | UUID FK → tenants | NOT NULL | -| `user_id` | UUID FK → profiles | | +| `user_id` | UUID FK → auth.users | | | `plan_id` | INTEGER FK → plans | | -| `status` | VARCHAR(50) | `active`, `cancelled`, `expired` | -| `start_date` | TIMESTAMPTZ | | -| `end_date` | TIMESTAMPTZ | | -| `stripe_subscription_id` | TEXT | | -| `created_at` | TIMESTAMPTZ | | +| `subscription_status` | VARCHAR | **NOT `status`** | +| `transaction_id` | INTEGER FK → transactions | The purchase that created it | +| `start_date` / `end_date` | TIMESTAMPTZ | | +| `current_period_start` / `current_period_end` | TIMESTAMPTZ | | +| `trial_start` / `trial_end` | TIMESTAMPTZ | | +| `cancel_at` | TIMESTAMPTZ | NOT NULL — must be advanced on renewal, or reactivation breaks | +| `cancel_at_period_end` | BOOLEAN | Student self-cancel flow | +| `canceled_at` / `ended_at` | TIMESTAMPTZ | | +| `superseded_by` | INTEGER | Set on plan change — points at the replacement subscription | +| `payment_provider` | VARCHAR | | +| `provider_subscription_id` | TEXT | **NOT `stripe_subscription_id`** | +| `provider_metadata` | JSONB | | +| `created` | TIMESTAMPTZ | **NOT `created_at`** | #### `transactions` Payment transactions. Tenant-scoped. @@ -463,11 +546,29 @@ Payment transactions. Tenant-scoped. | `transaction_date` | TIMESTAMPTZ | | | `payment_method` | VARCHAR(50) | | | `status` | `transaction_status` | `pending`, `successful`, `failed`, `archived`, `canceled`, `refunded` | -| `currency` | `currency_type` | `usd`, `eur` | +| `currency` | `currency_type` | `usd`, `eur`, `mxn`, `cop`, `clp`, `pen`, `ars`, `brl` | +| `payment_provider` | VARCHAR | | +| `provider_charge_id` | TEXT | Idempotency key for webhooks / on-chain confirms | +| `provider_subscription_id` | TEXT | | +| `provider_metadata` | JSONB | | +| `stripe_payment_intent_id` | TEXT | Stripe-specific, kept for history | +| `school_percentage_snapshot` | NUMERIC | Revenue split frozen at purchase time — database-owned, never caller-supplied | +| `settlement_currency` / `settlement_base` / `settlement_mint` / `settlement_sol_usd` | | Crypto settlement detail (Solana) | **Status is `'successful'`** (NOT `'succeeded'`). -Partial unique index: `(user_id, product_id, plan_id) WHERE status IN ('pending', 'successful')` — allows retries after failed payments. +**Three unique indexes, not one** — a purchase is either product-shaped or plan-shaped, never both: + +```sql +transactions_unique_product UNIQUE (user_id, product_id) + WHERE plan_id IS NULL AND status IN ('pending','successful') +transactions_unique_plan UNIQUE (user_id, plan_id) + WHERE product_id IS NULL AND status IN ('pending','successful') +transactions_provider_charge_id_unique UNIQUE (payment_provider, provider_charge_id) + WHERE provider_charge_id IS NOT NULL AND status = 'successful' -- webhook/Solana idempotency +``` + +Failed payments fall outside the predicate, so retries are allowed. #### `payment_requests` Manual/offline payment requests. Tenant-scoped. @@ -640,12 +741,12 @@ Storage bucket: `landing-page-assets` (public, 5MB limit, images only). ### Gamification -12 tables, all prefixed `gamification_`. Tenant-scoped (via migration) except `gamification_levels` which is **global**. +**10** tables prefixed `gamification_`. Tenant-scoped except `gamification_levels`, which is **global**. | Table | Purpose | |-------|---------| | `gamification_levels` | **GLOBAL** — level thresholds and titles | -| `gamification_profiles` | Per-user XP, level, streaks. `UNIQUE(user_id)` | +| `gamification_profiles` | Per-user XP, level, streaks. Created lazily by `award_xp()` on first award. **No `coins` column** — balance = `floor(total_xp/10) - total_coins_spent` | | `gamification_xp_transactions` | XP earn/spend log | | `gamification_achievements` | Achievement definitions (bronze → diamond tiers) | | `gamification_user_achievements` | Earned achievements per user | @@ -654,8 +755,10 @@ Storage bucket: `landing-page-assets` (public, 5MB limit, images only). | `gamification_user_rewards` | Active rewards (e.g., double XP) | | `gamification_leaderboard_cache` | Cached leaderboard rankings | | `gamification_challenge_participants` | Challenge participation | -| `gamification_daily_caps` | Daily XP caps | -| `gamification_challenges` | Challenge definitions | + +> `gamification_daily_caps` and `gamification_challenges` were documented here previously but **do not exist** in the database — don't query them. + +Weekly leagues live in separate tables: `league_tiers` (global tier definitions) and `league_memberships` (per-user, per-week standings). See [GAMIFICATION.md](./GAMIFICATION.md). --- @@ -742,37 +845,42 @@ Manual bank transfer requests for plan upgrades (LATAM schools). | Function | Purpose | |----------|---------| -| `enroll_user(_user_id UUID, _product_id INTEGER)` | Enrolls user in ALL courses linked to product (loops via `product_courses`) | -| `handle_new_subscription(_user_id UUID, _plan_id INTEGER)` | Creates subscription and enrolls in plan courses | +| `enroll_user(_user_id uuid, _product_id integer)` | Grants entitlements for **ALL** courses linked to the product (FOR loop over `product_courses`) | +| `handle_new_subscription(_user_id uuid, _plan_id integer, _transaction_id integer, _start_date timestamptz)` | Creates the subscription and writes entitlements for the plan's courses. Trigger-invoked — note the 4th arg | +| `has_course_access(_user_id uuid, _course_id integer)` | Access check against `entitlements`. Second arg is `integer` — cast `::int` when calling from SQL. Has no staff branch: teachers/admins are not implicitly granted access by this function | | `trigger_manage_transactions()` | **Trigger** on transaction status → `'successful'`: calls `enroll_user()` or `handle_new_subscription()` | ### Exams | Function | Purpose | |----------|---------| -| `create_exam_submission(student_id, exam_id, answers)` | Creates exam submission, returns `submission_id` | -| `save_exam_feedback(submission_id, exam_id, student_id, answers, overall_feedback, score)` | Saves AI feedback to submission | +| `create_exam_submission(p_student_id uuid, p_exam_id integer, p_answers jsonb)` | Creates exam submission, returns `submission_id` | +| `save_exam_feedback(p_submission_id integer, p_exam_id integer, p_student_id uuid, p_answers jsonb, p_overall_feedback text, p_score numeric, p_question_feedback jsonb, p_ai_model varchar, p_processing_time_ms integer)` | Saves AI feedback. **All params are `p_`-prefixed and there are nine of them** | ### Gamification | Function | Purpose | |----------|---------| -| `award_xp(_user_id, _action_type, _xp_amount, _reference_id, _reference_type)` | Awards XP, updates streaks, levels up | +| `award_xp(_user_id uuid, _action_type text, _xp_amount integer, _reference_id text, _reference_type text)` | Awards XP, updates streaks, levels up. Creates the gamification profile lazily via UPSERT | +| `award_xp(…, _tenant_id uuid)` | Overload — same, scoped to an explicit tenant. Trigger functions call this one | ### Certificates | Function | Purpose | |----------|---------| -| `check_and_issue_certificate()` | `SECURITY DEFINER` — checks completion and issues certificate | -| `calculate_course_completion()` | `SECURITY DEFINER` — calculates % completion for a course | +| `issue_certificate_if_eligible(p_user_id uuid, p_course_id integer)` | `SECURITY DEFINER` — the auto-issue path. **Template-gated**: with no active `certificate_templates` row for the course, nothing is issued (this is why 100% completion can produce no certificate) | +| `check_and_issue_certificate(p_user_id uuid, p_course_id integer)` | `SECURITY DEFINER` — checks completion and issues | +| `calculate_course_completion(p_user_id uuid, p_course_id integer)` | `SECURITY DEFINER` — % completion for a course | ### Platform | Function | Purpose | |----------|---------| -| `get_plan_features(_tenant_id UUID)` | Returns `{plan, plan_name, features, limits, transaction_fee_percent}` for feature gating | -| `get_platform_stats()` | Returns aggregate platform statistics for super admin dashboard | -| `validate_mcp_api_token(token_input TEXT)` | Validates API token, returns user info | +| `get_plan_features(_tenant_id uuid)` | Returns `{plan, plan_name, features, limits, transaction_fee_percent}` for feature gating — single source of truth | +| `get_gamification_features(_tenant_id uuid)` | Gamification feature flags; reads `platform_plans.features` the same way | +| `get_tenant_id()` | Resolves the caller's tenant inside RLS policies (JWT claim → anon header → NULL). Fails closed | +| `get_platform_stats()` | Aggregate platform statistics for the super admin dashboard | +| `validate_mcp_api_token(token_input text)` | Validates an API token, returns user info | --- @@ -849,8 +957,69 @@ const supabase = createAdminClient() ## Adding New Tables -1. Create migration: `supabase migration new add_table_name` -2. Write SQL with `tenant_id` column (if tenant-scoped) -3. Add RLS policies -4. Apply: `supabase db push` -5. Update types: `supabase gen types typescript --local > types/database.ts` +1. Create the migration: `supabase migration new add_table_name` +2. Write the SQL, including a `tenant_id UUID NOT NULL REFERENCES tenants(id)` column if the table is tenant-scoped +3. `ALTER TABLE … ENABLE ROW LEVEL SECURITY` **and** add policies — in the same migration +4. Verify from scratch locally: `npm run db:reset` +5. Regenerate types: `npm run db:types` → writes `lib/database.types.ts`. Commit it with the migration +6. Apply to cloud: `npm run db:push` + +> Permissive RLS policies **OR** together — adding a second policy widens access, it never narrows it. To tighten an existing policy you must `DROP POLICY` then `CREATE POLICY`. + +--- + +## Tables without `tenant_id` + +Adding `.eq('tenant_id', …)` to any of these **errors the whole query** — a common cause of blank pages. Isolation for the child tables comes from RLS through their parent row. + +`aristotle_messages`, `assignments`, `certificate_shares`, `certificate_verification_log`, `chats`, `comment_flags`, `comment_reactions`, `comments`, `community_poll_options`, `content_versions`, `device_push_tokens`, `exam_ai_configs`, `exam_answers`, `exam_question_scores`, `exam_questions`, `exam_scores`, `exam_views`, `exercise_code_student_submissions`, `exercise_completions`, `exercise_files`, `exercise_messages`, `gamification_levels`, `grades`, `issuer_keys`, `landing_page_templates`, `league_tiers`, `lesson_comments`, `lesson_completions`, `lesson_passed`, `lesson_views`, `lessons_ai_task_messages`, `lessons_ai_tasks`, `mcp_api_tokens`, `mcp_audit_log`, `messages`, `notification_preferences`, `permissions`, `plan_courses`, `platform_plans`, `profiles`, `question_options`, `reviews`, `role_permissions`, `roles`, `submissions`, `super_admins`, `system_settings`, `teacher_preview_sessions`, `tenants`, `ticket_messages`, `tickets`, `user_notifications`, `user_roles`, `user_ui_state`, `webhook_events` + +Regenerate that list any time: + +```sql +SELECT t.table_name FROM information_schema.tables t +WHERE t.table_schema = 'public' AND t.table_type = 'BASE TABLE' + AND NOT EXISTS (SELECT 1 FROM information_schema.columns c + WHERE c.table_schema = 'public' AND c.table_name = t.table_name + AND c.column_name = 'tenant_id') +ORDER BY 1; +``` + +--- + +## Verifying this document + +This page is hand-maintained and **will** drift — the schema is 116 tables across 168 migrations. Treat it as orientation, and confirm anything load-bearing against the database itself. The authoritative artifacts, in order: + +1. **`lib/database.types.ts`** — generated, always correct if regenerated. Grep it first. +2. **The live database** — introspect it directly. +3. **This document** — curated context and pitfalls that the generated types can't express. + +```bash +# Is the committed type file in sync? (empty diff = yes; the only expected +# difference is the __InternalSupabase header that --linked adds) +npx supabase gen types typescript --local --schema public > /tmp/types.ts +diff /tmp/types.ts lib/database.types.ts + +# Regenerate the committed file from the linked cloud project +npm run db:types + +# Introspect the local database (no psql binary needed) +docker exec -i supabase_db_lms-front psql -U postgres -d postgres -c "\d courses" +docker exec -i supabase_db_lms-front psql -U postgres -d postgres \ + -c "SELECT column_name, data_type FROM information_schema.columns + WHERE table_name = 'subscriptions' ORDER BY ordinal_position;" + +# Every enum and its values +docker exec -i supabase_db_lms-front psql -U postgres -d postgres \ + -c "SELECT t.typname, string_agg(e.enumlabel, ', ' ORDER BY e.enumsortorder) + FROM pg_type t JOIN pg_enum e ON e.enumtypid = t.oid GROUP BY 1 ORDER BY 1;" + +# Exact RPC signatures before calling one +docker exec -i supabase_db_lms-front psql -U postgres -d postgres \ + -c "SELECT p.proname, pg_get_function_identity_arguments(p.oid) FROM pg_proc p + JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = 'public' + AND p.proname = 'award_xp';" +``` + +If you find this page wrong, fix it in the same PR — that is how it stays useful. diff --git a/docs/OPERATIONS_GUIDE.md b/docs/OPERATIONS_GUIDE.md index 534d9bb9..9e0d94ef 100644 --- a/docs/OPERATIONS_GUIDE.md +++ b/docs/OPERATIONS_GUIDE.md @@ -121,7 +121,7 @@ CRON_SECRET=any-random-secret supabase db push ``` -This creates all 65+ tables, RLS policies, indexes, RPCs, and triggers. +This creates all 116 tables, RLS policies, indexes, RPCs, and triggers. ### 2e. Deploy Supabase Edge Functions (for AI + gamification) diff --git a/docs/PROJECT_OVERVIEW.md b/docs/PROJECT_OVERVIEW.md index 1b2da01f..1fc5a6fe 100644 --- a/docs/PROJECT_OVERVIEW.md +++ b/docs/PROJECT_OVERVIEW.md @@ -29,7 +29,7 @@ A multi-tenant SaaS Learning Management System. Schools operate as independent t | UI | Shadcn UI (base-mira variant), Tailwind CSS v4 | | Icons | Tabler Icons, Lucide React | | Fonts | Noto Sans (body), Geist Sans/Mono (UI/code) | -| Database | Supabase (PostgreSQL 15, 65+ tables) | +| Database | Supabase (PostgreSQL 15, 116 tables) | | Auth | Supabase Auth with JWT custom claims | | Storage | Supabase Storage | | AI | Vercel AI SDK with OpenAI (gpt-5-mini) | @@ -120,7 +120,7 @@ Manual/offline payment flow: student submits payment request, admin confirms, sy ## Database -65+ tables organized into key groups: +116 tables organized into key groups: | Group | Key Tables | |-------|-----------| @@ -143,7 +143,7 @@ All tenant-scoped tables use RLS. Direct queries with explicit `tenant_id` filte | Phase | Scope | Highlights | |-------|-------|-----------| | 1 | Foundation | Next.js 16, Supabase, Shadcn UI (base-mira), Tailwind CSS v4 | -| 2 | Database | 65+ tables, RLS policies, database functions and RPCs | +| 2 | Database | 116 tables, RLS policies, database functions and RPCs | | 3 | Core LMS | Auth, enrollment, lessons, exams, progress tracking | | 4 | AI Integration | Vercel AI SDK with OpenAI (gpt-5-mini), Aristotle AI Tutor, exam grading | | 5 | Gamification | 12 tables: XP, levels, streaks, achievements, leaderboard, point store | diff --git a/docs/README.md b/docs/README.md index 4f3e91e6..53020445 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,64 +1,67 @@ -# LMS V2 Documentation +# Documentation -Welcome to the LMS V2 documentation! This documentation is designed to help developers and AI agents understand the project architecture, database schema, and development workflow. +Index of the `docs/` folder. Every link here points at a file that exists — if you find a dead link or a stale claim, fix it in the same PR. -## 📚 Documentation Structure +## Start here -### For Getting Started -- **[Project Overview](./PROJECT_OVERVIEW.md)** - High-level project goals and architecture -- **[Getting Started](./GETTING_STARTED.md)** - Setup guide for new developers -- **[Development Workflow](./DEVELOPMENT_WORKFLOW.md)** - Day-to-day development practices +| Doc | What it covers | +|--|--| +| [GETTING_STARTED.md](./GETTING_STARTED.md) | **Run the project**: install, `.env.local`, Supabase, seed data, dev server, tests, troubleshooting | +| [../CLAUDE.md](../CLAUDE.md) | Condensed architecture reference + the known-pitfalls list. The single most useful page for a new contributor | +| [PROJECT_OVERVIEW.md](./PROJECT_OVERVIEW.md) | High-level goals and architecture | +| [DEVELOPMENT_WORKFLOW.md](./DEVELOPMENT_WORKFLOW.md) | Day-to-day practices, branching, PRs | -### Technical Reference -- **[Database Schema](./DATABASE_SCHEMA.md)** - Complete database structure and relationships -- **[Authentication & Authorization](./AUTH.md)** - How auth and role-based access works -- **[API Routes](./API_ROUTES.md)** - API endpoints and their purposes -- **[RLS Policies](./RLS_POLICIES.md)** - Row Level Security implementation (TODO) +## Technical reference -### Feature Documentation -- **[Student Dashboard](./features/STUDENT_DASHBOARD.md)** - Student experience and features (TODO) -- **[Teacher Dashboard](./features/TEACHER_DASHBOARD.md)** - Teacher tools and workflows (TODO) -- **[Admin Dashboard](./features/ADMIN_DASHBOARD.md)** - Admin capabilities (TODO) -- **[AI Integration](./features/AI_INTEGRATION.md)** - How AI is used in the platform (TODO) +| Doc | What it covers | +|--|--| +| [DATABASE_SCHEMA.md](./DATABASE_SCHEMA.md) | Tables, columns, enums, RPCs, RLS patterns — plus how to verify it against the live DB | +| [MIGRATIONS.md](./MIGRATIONS.md) | Writing and applying migrations | +| [AUTH.md](./AUTH.md) | Auth flows, JWT hook, role resolution | +| [API_REFERENCE.md](./API_REFERENCE.md) | API routes and their purposes | +| [ENV_VARIABLES.md](./ENV_VARIABLES.md) | Environment variables in detail | +| [I18N_GUIDE.md](./I18N_GUIDE.md) | Adding translatable copy (en/es) | +| [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) | Common issues and fixes | -### For AI Agents -- **[AI Agent Guide](./AI_AGENT_GUIDE.md)** - Special instructions for AI assistants working on this codebase -- **[Common Tasks](./COMMON_TASKS.md)** - Frequent development tasks and how to accomplish them -- **[Troubleshooting](./TROUBLESHOOTING.md)** - Common issues and solutions +## Features -## 🎯 Quick Links +| Doc | What it covers | +|--|--| +| [MONETIZATION.md](./MONETIZATION.md) | School billing, feature gating, LATAM payments, revenue dashboard | +| [PROVIDER_AGNOSTIC_PAYMENTS_SPIKE.md](./PROVIDER_AGNOSTIC_PAYMENTS_SPIKE.md) | The payment-provider contract (Stripe, PayPal, Lemon Squeezy, Solana, Binance) | +| [MANUAL_PAYMENT_SYSTEM.md](./MANUAL_PAYMENT_SYSTEM.md) | Offline / bank-transfer payment flow | +| [GAMIFICATION.md](./GAMIFICATION.md) | XP, levels, streaks, achievements, store, leagues | +| [COMMUNITY_SPACES.md](./COMMUNITY_SPACES.md) | Community feed, comments, reactions, polls, moderation | +| [LANDING_PAGE_BUILDER.md](./LANDING_PAGE_BUILDER.md) · [AI_LANDING_PAGE_GENERATION.md](./AI_LANDING_PAGE_GENERATION.md) | Puck visual editor and the AI page generator | +| [AI_INTEGRATION.md](./AI_INTEGRATION.md) | AI tutor, grading, and exercise evaluation | +| [GUIDED_TOURS.md](./GUIDED_TOURS.md) | Onboarding tours (driver.js) | +| [MDX_COMPONENTS.md](./MDX_COMPONENTS.md) | Lesson content blocks | +| [PLATFORM_ADMIN.md](./PLATFORM_ADMIN.md) · [PLATFORM_SETTINGS_GUIDE.md](./PLATFORM_SETTINGS_GUIDE.md) | Super-admin panel | +| [MCP_SETUP.md](./MCP_SETUP.md) | MCP server for AI agents | -### Need to... -- **Add a new table?** → See [Database Schema](./DATABASE_SCHEMA.md#adding-new-tables) -- **Create a new dashboard page?** → See [Development Workflow](./DEVELOPMENT_WORKFLOW.md#creating-pages) -- **Add RLS policies?** → See [RLS Policies](./RLS_POLICIES.md) -- **Integrate AI features?** → See [AI Integration](./features/AI_INTEGRATION.md) -- **Debug auth issues?** → See [Authentication](./AUTH.md#troubleshooting) +## Operations -## 🏗️ Project Status +| Doc | What it covers | +|--|--| +| [OPERATIONS_GUIDE.md](./OPERATIONS_GUIDE.md) | Running the platform | +| [DEPLOYMENT.md](./DEPLOYMENT.md) · [DOCKER_DEPLOYMENT.md](./DOCKER_DEPLOYMENT.md) | Deploying | +| [CLOUDFLARE_WILDCARD_SETUP.md](./CLOUDFLARE_WILDCARD_SETUP.md) | Wildcard DNS for tenant subdomains | -**Current Phase**: Phase 5 Complete -- ✅ Phase 1: Fresh Next.js 16 + Shadcn UI (Lyra theme) -- ✅ Phase 2: Complete database schema (pulled from cloud) -- ✅ Phase 3: Authentication with role-based routing -- ✅ Phase 4: Basic Stripe payment integration -- ✅ Phase 5: Student Dashboard (complete with lessons, exams, progress tracking) -- 🔄 Phase 6: Teacher Dashboard (next priority) -- ⏳ Phase 7-11: Admin, Features, i18n, AI Docs, Testing +## For AI agents -See [PHASE_5_SUMMARY.md](./PHASE_5_SUMMARY.md) for latest completion details. +| Doc | What it covers | +|--|--| +| [AI_AGENT_GUIDE.md](./AI_AGENT_GUIDE.md) | Patterns and conventions for AI assistants | +| [COMMON_TASKS.md](./COMMON_TASKS.md) | Frequent development tasks, step by step | -## 🤝 Contributing +## Need to… -When contributing to this project: -1. Read the [Development Workflow](./DEVELOPMENT_WORKFLOW.md) -2. Understand the [Database Schema](./DATABASE_SCHEMA.md) -3. Follow the established patterns (see [AI Agent Guide](./AI_AGENT_GUIDE.md)) -4. Test thoroughly before committing +- **Run the project for the first time?** → [GETTING_STARTED.md](./GETTING_STARTED.md) +- **Add a table or migration?** → [DATABASE_SCHEMA.md § Adding New Tables](./DATABASE_SCHEMA.md#adding-new-tables) and [MIGRATIONS.md](./MIGRATIONS.md) +- **Check a column name?** → Grep `lib/database.types.ts` (generated, always correct), then [DATABASE_SCHEMA.md § Verifying this document](./DATABASE_SCHEMA.md#verifying-this-document) +- **Debug auth or roles?** → [AUTH.md](./AUTH.md), then the pitfalls list in [../CLAUDE.md](../CLAUDE.md) +- **Add a payment provider?** → [PROVIDER_AGNOSTIC_PAYMENTS_SPIKE.md](./PROVIDER_AGNOSTIC_PAYMENTS_SPIKE.md) -## 📞 Support +## A note on the rest of this folder -For questions or issues: -- Check [Troubleshooting](./TROUBLESHOOTING.md) -- Review the relevant feature documentation -- Consult the [AI Agent Guide](./AI_AGENT_GUIDE.md) for AI-specific help +`docs/` also contains dated implementation summaries, test reports, audits and phase write-ups (`PHASE_*`, `*_SUMMARY.md`, `*_REPORT.md`, `FEBRUARY_2026_*`, `ENTITLEMENTS_MIGRATION_PLAN.md`, `ACTUAL_SCHEMA.md`, …). Those are **historical records of a moment in time**, not maintained references — several describe schema that has since changed. Use them for context on *why* something was done; never as the current source of truth. `docs/adr/` holds architecture decision records, `docs/plans/` and `docs/refactor-plans/` hold planning documents.