Skip to content

Commit 52ab5e9

Browse files
docs: correct schema reference against the live database (#530)
Verified docs/DATABASE_SCHEMA.md table by table against the local database (which matches cloud — see below) and fixed what was wrong. lib/database.types.ts needed no change: regenerating it from the local DB produces a byte-identical file apart from the __InternalSupabase header that `--linked` adds. Local migrations, cloud schema and the committed types are all in sync. Columns that did not exist as documented (each one silently breaks a query or a .rpc() call): - enrollments: still listed product_id/subscription_id and their CHECK constraint, all dropped by the entitlements migration. entitlements itself was absent from the doc entirely despite being the source of truth for course access. - plans: documented `name`, `interval`, `stripe_*_id`; actually plan_name, duration_in_days, provider_*_id. - subscriptions: documented `status`, `stripe_subscription_id`, `created_at`; actually subscription_status, provider_subscription_id, created. - exercise_completions: documented student_id + submission/is_correct/ ai_feedback; actually user_id + completed_by/score. - exercises: documented initial_code/solution_code/test_cases — none exist. - products: documented stripe_product_id/stripe_price_id; actually provider_*_id. - exam_questions: documented a `sequence` column that does not exist. - exam_submissions: documented is_reviewed; actually review_status. - gamification: documented 12 tables including gamification_daily_caps and gamification_challenges, neither of which exists. There are 10. - transactions: documented one composite unique index; there are three (product-shaped, plan-shaped, provider_charge_id idempotency). - currency_type: documented usd/eur; there are eight values. - RPCs: handle_new_subscription is missing _start_date, save_exam_feedback takes nine p_-prefixed args not six, award_xp's signature was wrong in CLAUDE.md and AI_AGENT_GUIDE.md. - Table count was "65+" everywhere; it is 116. Also adds what the doc had no way to keep right on its own: - "Verifying this document" — the diff/introspection commands to check any claim here against the database in one line. - "Tables without tenant_id" — the full 55-table list plus the query that regenerates it. Filtering these by tenant_id errors the whole query, which is a recurring source of blank pages. - docs/README.md rewritten: it indexed six files that do not exist (API_ROUTES.md, RLS_POLICIES.md, four features/*.md) and advertised the project as "Phase 5 complete, Phase 6 next". - docs/ACTUAL_SCHEMA.md now carries a stale banner — it claims to be the actual live schema and is the first thing a name-search finds, but it is a February 2026 snapshot documenting dropped columns. Claude-Session: https://claude.ai/code/session_01853LDokPbhv9Kg9Ctz9DB5 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent be37e8f commit 52ab5e9

7 files changed

Lines changed: 357 additions & 165 deletions

File tree

CLAUDE.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,17 +116,18 @@ All routes live under `app/[locale]/` (`[locale]` is always `/en/` or `/es/`). P
116116

117117
### Database Schema Essentials
118118

119-
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.
119+
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.
120120

121121
`profiles` and `gamification_levels` are global (no `tenant_id`).
122122

123123
**Key RPCs:**
124124
```typescript
125125
supabase.rpc('enroll_user', { _user_id, _product_id })
126-
supabase.rpc('handle_new_subscription', { _user_id, _plan_id, _transaction_id }) // trigger-invoked; writes to entitlements
127-
supabase.rpc('award_xp', { p_user_id, p_action_type, p_reference_id })
126+
supabase.rpc('handle_new_subscription', { _user_id, _plan_id, _transaction_id, _start_date }) // trigger-invoked; writes to entitlements
127+
supabase.rpc('has_course_access', { _user_id, _course_id }) // access check; _course_id is integer — cast ::int from SQL
128+
supabase.rpc('award_xp', { _user_id, _action_type, _xp_amount, _reference_id, _reference_type }) // overload adds _tenant_id
128129
supabase.rpc('create_exam_submission', { p_student_id, p_exam_id, p_answers })
129-
supabase.rpc('save_exam_feedback', { submission_id, exam_id, student_id, answers, overall_feedback, score })
130+
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 })
130131
supabase.rpc('get_plan_features', { _tenant_id })
131132
```
132133

@@ -151,16 +152,14 @@ NEXT_PUBLIC_PLATFORM_DOMAIN= # e.g. lvh.me for local dev, lmsplatform.com
151152

152153
## Testing
153154

154-
E2E tests in `tests/playwright/`, four files by priority:
155-
- `multi-tenant-isolation.spec.ts` — P0, 8 tests
156-
- `authentication-security.spec.ts` — P0, 6 tests
157-
- `payment-security.spec.ts` — P0, 7 tests
158-
- `comprehensive-security-audit.spec.ts` — P1/P2, 26 tests
155+
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`.
156+
157+
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/`).
159158

160159
Test accounts (from `supabase/seed.sql`, seeded by `supabase db reset`):
161160
- `student@e2etest.com` / `password123` — student (Default School)
162-
- `owner@e2etest.com` / `password123` — admin (Default School)
163-
- `creator@codeacademy.com` / `password123`teacher (Code Academy, subdomain `code-academy.lvh.me:3000`)
161+
- `owner@e2etest.com` / `password123` — admin (Default School) **+ super admin** (`/platform/*`)
162+
- `creator@codeacademy.com` / `password123`**admin** (Code Academy, subdomain `code-academy.lvh.me:3000`)
164163
- `alice@student.com` / `password123` — student (Code Academy)
165164

166165
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
174173
- **`profiles` has no `email`** — get emails via `createAdminClient().auth.admin.getUserById()`.
175174
- **`exam_submissions` order column** is `submission_date`, not `submitted_at`.
176175
- **Transaction status** is `'successful'`, not `'succeeded'`.
177-
- 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.
176+
- 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`**.
177+
- **`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.
178+
- **`subscriptions` uses `subscription_status`** (not `status`), `provider_subscription_id` (not `stripe_subscription_id`), and `created` (not `created_at`).
179+
- **`plans` uses `plan_name` and `duration_in_days`** — there is no `name`, no `interval`, no `status`. Soft-deleted via `deleted_at`.
180+
- **`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`.
178181
- **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.
179182
- **`proxy.ts` is the only middleware** — do not create `middleware.ts` (conflict).
180183
- **`createAdminClient()`** lives in `@/lib/supabase/admin`, NOT `@/lib/supabase/server`.

docs/ACTUAL_SCHEMA.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
# Actual Database Schema Reference
22

3-
This file contains the **actual** column names from the live database (discovered 2026-02-08).
3+
> ## ⚠️ OUT OF DATE — do not trust this file
4+
>
5+
> 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`).
6+
>
7+
> Current sources of truth, in order:
8+
> 1. **`lib/database.types.ts`** — generated from the database, always correct
9+
> 2. **The live database** — see [DATABASE_SCHEMA.md § Verifying this document](./DATABASE_SCHEMA.md#verifying-this-document)
10+
> 3. **[DATABASE_SCHEMA.md](./DATABASE_SCHEMA.md)** — curated and maintained
11+
>
12+
> Kept only as a historical record of what the schema looked like in February 2026.
13+
14+
This file contains the column names discovered in the live database on 2026-02-08.
415

516
## Core Tables
617

docs/AI_AGENT_GUIDE.md

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -302,26 +302,32 @@ lessons, exercises, exams (tenant-scoped)
302302
303303
### Key Functions to Know
304304
305-
1. **`enroll_user(_user_id, _product_id)`**
306-
- Enrolls user in ALL courses linked to product (loops through `product_courses`)
307-
- Sets `status = 'active'` and inherits `tenant_id` from product
305+
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)).
306+
307+
1. **`enroll_user(_user_id uuid, _product_id integer)`**
308+
- Grants access to ALL courses linked to the product (loops through `product_courses`)
309+
- Writes to **`entitlements`** — the source of truth for access — inheriting `tenant_id` from the product
308310
- Called automatically on successful payment
309311
310-
2. **`get_plan_features(_tenant_id)`**
312+
2. **`has_course_access(_user_id uuid, _course_id integer)`**
313+
- The access check. Second arg is `integer` — cast `::int` from SQL
314+
- No staff branch: teachers/admins are not implicitly granted access here
315+
316+
3. **`get_plan_features(_tenant_id uuid)`**
311317
- Returns plan info, features (JSONB), and limits for the tenant
312318
- Single source of truth for feature gating
313319
- `SECURITY DEFINER` — works regardless of caller's RLS context
314320
315-
3. **`create_exam_submission(student_id, exam_id, answers)`**
316-
- Creates exam submission
317-
- Returns submission ID
321+
4. **`create_exam_submission(p_student_id uuid, p_exam_id integer, p_answers jsonb)`**
322+
- Creates exam submission, returns `submission_id`
318323
319-
4. **`save_exam_feedback(submission_id, ...)`**
320-
- Saves AI feedback to exam
321-
- Updates score and marks as reviewed
324+
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)`**
325+
- Saves AI feedback to the exam and updates the score
326+
- Nine params, all `p_`-prefixed
322327
323-
5. **`award_xp(p_user_id, p_action_type, p_reference_id)`**
324-
- Awards XP for gamification actions
328+
6. **`award_xp(_user_id uuid, _action_type text, _xp_amount integer, _reference_id text, _reference_type text)`**
329+
- Awards XP for gamification actions; creates the gamification profile lazily
330+
- An overload takes a trailing `_tenant_id uuid` — trigger functions call that one
325331
326332
### Querying with Relations
327333
@@ -377,7 +383,7 @@ If query returns empty when it shouldn't:
377383
4. Verify user has required enrollment/role
378384
5. Test with `createAdminClient()` to bypass RLS (temporarily, for debugging only)
379385
380-
**RLS is enabled on ALL tenant-scoped tables** (65+ tables). Standard policy pattern:
386+
**RLS is enabled on ALL tenant-scoped tables** (116 tables in `public`, 61 of them carrying a `tenant_id`). Standard policy pattern:
381387
- SELECT: users who are members of the tenant (checked via `tenant_users`)
382388
- INSERT/UPDATE/DELETE: users with `teacher` or `admin` role in the tenant
383389
- Special cases: students can INSERT own `enrollments`, `lesson_completions`, `exam_submissions`

0 commit comments

Comments
 (0)