Started: 2026-03-13 Status: COMPLETE — All 38 tables secured
38 out of 92 public tables had NO RLS enabled. Both anon and authenticated roles had full CRUD + TRUNCATE on all of them. This is a critical multi-tenant data leak.
| # | Table | Batch | Status | Notes |
|---|---|---|---|---|
| 1 | transactions |
1 | ✅ Done | Payment data — highest priority |
| 2 | courses |
2 | ✅ Done | |
| 3 | enrollments |
2 | ✅ Done | |
| 4 | products |
3 | ✅ Done | |
| 5 | subscriptions |
3 | ✅ Done | |
| 6 | plans |
3 | ✅ Done | |
| 7 | exams |
4 | ✅ Done | |
| 8 | exam_submissions |
4 | ✅ Done | |
| 9 | exercises |
5 | ✅ Done | |
| 10 | lessons |
5 | ✅ Done | |
| 11 | product_courses |
6 | ✅ Done | |
| 12 | course_categories |
6 | ✅ Done |
| # | Table | Batch | Status | Notes |
|---|---|---|---|---|
| 1 | grades |
12 | ✅ Done | user_id scoped |
| 2 | exam_answers |
7 | ✅ Done | |
| 3 | exam_questions |
7 | ✅ Done | |
| 4 | exam_scores |
7 | ✅ Done | student_id scoped |
| 5 | question_options |
7 | ✅ Done | |
| 6 | exercise_code_student_submissions |
8 | ✅ Done | user_id scoped |
| 7 | exercise_completions |
8 | ✅ Done | user_id scoped |
| 8 | exercise_files |
8 | ✅ Done | |
| 9 | exercise_messages |
8 | ✅ Done | user_id scoped |
| 10 | lesson_comments |
9 | ✅ Done | user_id scoped |
| 11 | lesson_passed |
9 | ✅ Done | user_id scoped |
| 12 | lesson_views |
9 | ✅ Done | user_id scoped |
| 13 | tickets |
10 | ✅ Done | user_id scoped |
| 14 | ticket_messages |
10 | ✅ Done | user_id scoped |
| 15 | submissions |
10 | ✅ Done | student_id scoped |
| 16 | messages |
11 | ✅ Done | |
| 17 | chats |
11 | ✅ Done | user_id scoped |
| 18 | comment_flags |
11 | ✅ Done | user_id scoped |
| 19 | comment_reactions |
11 | ✅ Done | user_id scoped |
| 20 | assignments |
12 | ✅ Done | |
| 21 | roles |
12 | ✅ Done | Lookup — SELECT only |
| 22 | permissions |
12 | ✅ Done | Lookup — SELECT only |
| 23 | role_permissions |
12 | ✅ Done | Lookup — SELECT only |
| 24 | landing_page_templates |
12 | ✅ Done | Super admin managed |
| 25 | plan_courses |
12 | ✅ Done | Public read |
| 26 | exam_views |
7 | ✅ Done | user_id scoped |
Migration: rls_transactions
Policies added:
- SELECT: Students see own transactions in tenant
- SELECT: Teachers/admins see all transactions in tenant
- SELECT: Super admins see all
- INSERT: Authenticated users can create own transactions in tenant
- UPDATE: Authenticated users can update own transactions in tenant (for stripe_payment_intent_id)
- TRUNCATE revoked from anon + authenticated
Files that query transactions:
app/[locale]/dashboard/admin/transactions/page.tsx— SELECT, admin, server clientapp/[locale]/dashboard/admin/page.tsx— SELECT count, admin, server clientapp/[locale]/dashboard/student/profile/page.tsx— SELECT, student, server clientapp/[locale]/dashboard/teacher/revenue/page.tsx— SELECT, teacher, server clientapp/[locale]/dashboard/admin/users/[userId]/page.tsx— SELECT, admin, server clientapp/api/stripe/webhook/route.ts— UPDATE, system, service role (bypasses RLS)app/api/stripe/create-payment-intent/route.ts— INSERT + UPDATE, authenticated, server clientapp/actions/payment-requests.ts— INSERT, admin (uses both server + admin client)app/actions/admin/revenue.ts— SELECT, admin, server clientapp/[locale]/(public)/checkout/actions.ts— INSERT, authenticated, server clientapp/[locale]/platform/tenants/[tenantId]/page.tsx— SELECT, super admincomponents/teacher/transaction-list.tsx— client display component
Testing:
- Supabase API: anon cannot SELECT — returns
[] - Supabase API: anon cannot INSERT —
permission denied - Supabase API: anon cannot DELETE —
permission denied - Supabase API: authenticated student sees only own transactions
- Supabase API: authenticated admin sees all tenant transactions
- Supabase API: student cannot INSERT for another user —
violates row-level security - Supabase API: student cannot UPDATE another user's transaction — returns
[] - Supabase API: service role (webhooks) bypasses RLS correctly
- Playwright: admin transactions page loads (skipped — subdomain auth routing blocks Playwright browser; not an RLS issue)
Issues found:
- None — all API-level security tests passed
- Playwright browser testing deferred:
proxy.tssubdomain routing redirects unauthenticated Playwright sessions to root domain. This is expected behavior, not an RLS bug. Full E2E test will be done after all batches are complete.
Migration: rls_courses_enrollments
Courses policies: 4 SELECT (anon published, teacher own, admin tenant, super admin), 1 INSERT (teacher/admin), 2 UPDATE (teacher own, admin tenant), 1 DELETE (admin tenant). Anon revoked INSERT/UPDATE/DELETE/TRUNCATE.
Enrollments policies: 3 SELECT (student own, teacher/admin tenant, super admin), 1 INSERT (own), 1 UPDATE (admin). Anon revoked all writes.
Testing:
- Anon sees only published courses (4 returned, all
status=published) - Anon INSERT courses —
permission denied - Student sees published courses, cannot INSERT
- Admin sees all tenant courses
- Anon SELECT enrollments — empty
- Anon INSERT enrollments —
permission denied - Student sees only own enrollments (2 returned)
- Student INSERT for another user —
violates row-level security - Admin sees all tenant enrollments (4 returned)
Issues found: None
Migration: rls_products_subscriptions_plans
Testing:
- Products: anon sees active only, anon INSERT blocked
- Plans: anon sees non-deleted only, anon INSERT blocked
- Subscriptions: anon empty, student sees own, admin sees tenant
Issues found: None
Migration: rls_exams_exam_submissions. Tenant-scoped SELECT/INSERT/UPDATE. Anon blocked. Tested OK.
Migration: rls_exercises_lessons. Tenant-scoped. Anon blocked. Tested OK.
Migration: rls_product_courses_course_categories. Public SELECT for catalog. Admin CRUD. Anon write blocked. Tested OK.
Migration: rls_exam_child_tables. Tables: exam_questions, question_options, exam_answers, exam_scores, exam_views. User-scoped where applicable. Anon blocked. Tested OK.
Migration: rls_exercise_child_tables. Tables: exercise_code_student_submissions, exercise_completions, exercise_files, exercise_messages. User-scoped. Anon blocked. Tested OK.
Migration: rls_lesson_child_tables. Tables: lesson_comments, lesson_passed, lesson_views. User-scoped. Anon blocked. Tested OK.
Migration: rls_tickets_submissions. Tables: tickets, ticket_messages, submissions. User-scoped. Anon blocked. Tested OK.
Migration: rls_chats_comments. Tables: chats, messages, comment_flags, comment_reactions. User-scoped. Anon blocked. Tested OK.
Migration: rls_remaining_tables. Tables: grades, assignments, plan_courses, roles, permissions, role_permissions, landing_page_templates. Lookup tables = SELECT only. Sensitive tables = user-scoped. Anon write blocked. Tested OK.
Result: 0 tables without RLS. All 92 public tables now have RLS enabled.
Final anon security test: All 38 previously unprotected tables reject anon INSERT with permission denied (HTTP 401) or schema validation (HTTP 400). All sensitive tables return 0 rows for anon SELECT.
Known items for future review:
- Some policies use
get_tenant_role()from JWT claims which defaults to'student'— works correctly but could be tightened withtenant_userschecks in the future messagestable had a pre-existing SELECT policy that was unused (RLS was disabled) — now active
Tested all critical flows via Playwright MCP browser:
| Flow | Status | Notes |
|---|---|---|
| Student login + dashboard | PASS | Welcome, gamification, sidebar all load |
| Student browse courses | PASS | 2 courses visible, categories work |
| Teacher dashboard | PASS | Stats, courses, checklist all load with data |
| Admin transactions | PASS | Table structure, stats cards |
| Admin courses | PASS | 2 courses with full management table |
| Admin enrollments | PASS | 2 enrollments visible with correct data |
| Admin settings | PASS | All tabs and form fields render |
| Pricing (public) | PASS | Page loads, FAQ section present |
| Platform panel (super admin) | PASS | Overview with all stats |
| Tenant isolation | PASS | Non-member correctly gets join-school redirect |
No RLS-related breakage found.
-
Missing i18n keys for teacher dashboard tour — 10 MISSING_MESSAGE console errors
- Keys:
dashboard.teacher.tour.{welcome,stats,courses,sidebarCourses,sidebarCreate}.{title,description} - Fix: Added missing keys to
messages/en.jsonandmessages/es.json
- Keys:
-
Admin dashboard redirect to /onboarding — Test users had
onboarding_completed = falseowner@e2etest.comandcreator@codeacademy.comhit the onboarding redirect on admin dashboard- Fix: Set
onboarding_completed = truefor admin/teacher test users in DB and updatedsupabase/seed.sql