Use this document to continue development with another AI or developer. Read docs/uniflow-workflow.md for the full platform journey (Uniflow Admin → University Admin → Mobile App).
uniflow/
├── uniflow-web/ # Next.js 16 — admin portals + marketing
├── uniflow-app/ # Expo React Native — student/lecturer mobile
├── docs/
│ ├── uniflow-workflow.md # End-to-end platform workflow
│ ├── CONTINUATION.md # This file
│ └── course-offering-design.md # Courses ↔ timetable redesign
└── memory/architecture.md # May be stale; prefer docs/ above
Backend: Supabase (Auth + Postgres + RLS + Storage). No separate API server — Next.js route handlers use createAdminClient() for privileged ops.
Domains (production):
| Host | Role |
|---|---|
uniflowapp.xyz |
Marketing, Uniflow Admin login, reset-password landing |
admin.uniflowapp.xyz |
Uniflow Admin dashboard |
{short}-admin.uniflowapp.xyz |
University Admin portal |
| Role | Web | Mobile |
|---|---|---|
uniflow_admin |
✅ super dashboard | ❌ blocked |
university_admin |
✅ uni portal | ❌ blocked |
student |
❌ | ✅ (student) tabs |
lecturer, dean, hod |
❌ | ✅ (lecturer) tabs (same UI) |
Role gates: uniflow-web/src/lib/role-access.ts, uniflow-app/lib/role-access.ts
Password policy: No self-service forgot-password on web admin portals. Mobile has forgot-password. Admin-initiated reset emails only for web admins.
Old model had three disconnected tables (lecturer_courses, timetable, enrollments) with no enrollment UI, no session filtering, and lecturers using two different query paths on mobile.
course_offerings is the hub:
courses (catalog)
→ course_offerings (course + lecturer + department + session + semester)
→ timetable (slots: day, time, venue)
→ enrollments (students, auto by level)
-
Import students CSV (existing):
full_name, email, level, department_short_name -
Import one combined CSV on Timetable page:
course_code,course_title,level,semester,credit_units,lecturer_email,day,start_time,end_time,venue- Rows with schedule → offering + slot
- Rows without day/time → offering only (schedule later)
- Same
course_code+lecturer_emailrepeated = multiple slots (lecture + lab)
-
System auto-enrolls students matching
department + level + semester -
Mobile app shows courses/timetable immediately
| Area | Status | Key files |
|---|---|---|
| SQL migration | ✅ Written, must run in Supabase | uniflow-app/supabase/class_updates_migration.sql, course_offerings_migration.sql, *_rls*.sql |
| Combined CSV API | ✅ | uniflow-web/src/app/api/timetable/import/route.ts |
| Auto-enroll API | ✅ | uniflow-web/src/app/api/enrollments/auto/route.ts |
| Offerings list API | ✅ GET only | uniflow-web/src/app/api/course-offerings/route.ts |
| Combined import UI | ✅ On timetable page | uniflow-web/src/components/university/CombinedTimetableImport.tsx |
| Session helpers | ✅ | getAcademicContext() in web + mobile lib/academic.ts |
| Mobile hooks | ✅ Offering-aware + legacy fallback | useStudentEnrollments.ts, useLecturerCourseIds.ts |
| Mobile screens | ✅ Updated | student/lecturer courses, timetable, index, resources |
| Types | ✅ | uniflow-app/types/index.ts — CourseOffering, course_offering_id fields |
-
Run SQL migrations in Supabase SQL Editor (order matters):
class_updates_migration.sql(if you see "column class_updates.timetable_id does not exist", "Could not find the 'delay_minutes' column...", or 'null value in column "title" of relation "class_updates" violates not-null')course_offerings_migration.sqlcourse_offerings_rls.sql(orcourse_offerings_rls_clean.sql)
-
Refactor
u/coursespage — catalog only; remove lecturer assignment UI (now handled by combined CSV / offerings). -
Dedicated
u/offeringspage (optional) — list offerings, manual add, per-offering enrollment override UI. -
Manual enroll/unenroll API + UI —
POST /api/enrollmentsfor exceptions (plan item; not implemented). -
Remove legacy paths (phase 2):
- Drop
lecturer_coursesusage from web courses page - Remove
lecturer_coursestable after verification - Drop
timetable.course_id/timetable.lecturer_idnullable legacy columns
- Drop
-
Update
docs/uniflow-workflow.md— courses/timetable section still describes old model. -
Lecturer index (
uniflow-app/app/(lecturer)/index.tsx) — verify it uses offering-based timetable if it queries timetable directly. -
Tests — none exist; add smoke tests for CSV import + auto-enroll.
cd uniflow-web
cp .env.local.example .env.local # if exists
npm install
npm run dev # localhost:3000Env vars (typical):
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEY(server routes)NEXT_PUBLIC_BASE_DOMAIN(defaultuniflowapp.xyz)RESEND_API_KEY(emails)
cd uniflow-app
npm install
npx expo startEnv (eas.json / .env):
EXPO_PUBLIC_SUPABASE_URLEXPO_PUBLIC_SUPABASE_ANON_KEYEXPO_PUBLIC_WEB_APP_URL(for forgot-password API)
Execute SQL files in uniflow-app/supabase/ in Supabase Dashboard → SQL Editor. Order:
courses_rls.sqldepartment_levels.sqltimetable_department_fk.sqlprofiles_rls_fix.sqlclass_updates_migration.sql← ensures timetable_id + other columns (including delay_minutes + title) exist (fixes 42703 / PGRST204 / 23502 not-null errors)mobile_read_rls.sql(re-run to fix recursion in lecturer_courses policies)course_offerings_migration.sql← newcourse_offerings_rls.sql/course_offerings_rls_clean.sql(re-run; includes safe lecturer_courses policies)
Redirect URLs (Authentication → URL Configuration):
https://uniflowapp.xyz/reset-password**https://*-admin.uniflowapp.xyz/**
uniflow-web/src/proxy.ts— subdomain routing, auth guardsuniflow-web/src/lib/subdomain.ts— host parsinguniflow-web/src/app/api/auth/verify-portal/route.ts— login role check
| Path (external on subdomain) | File |
|---|---|
/ |
src/app/(university)/u/page.tsx |
/faculties |
u/faculties/page.tsx |
/departments |
u/departments/page.tsx |
/courses |
u/courses/page.tsx |
/timetable |
u/timetable/page.tsx |
/students |
u/students/page.tsx |
/lecturers |
u/lecturers/page.tsx |
| Endpoint | Purpose |
|---|---|
POST /api/create-staff |
Create student/lecturer + reset email |
POST /api/reset-password |
Admin-initiated password reset |
POST /api/approve-university |
Onboard new university |
POST /api/timetable/import |
Combined CSV preview/commit |
POST /api/enrollments/auto |
Auto-enroll by dept/level |
GET /api/course-offerings |
List offerings |
uniflow-app/store/useAuthStore.ts— sign-in, hydrate, role blockuniflow-app/app/_layout.tsx— AuthGuard routes to(student)or(lecturer)
hooks/useStudentEnrollments.ts→{ courseIds, offeringIds }hooks/useLecturerCourseIds.ts→{ courseIds, offeringIds }(queriescourse_offerings, falls back tolecturer_courses)lib/timetable-query.ts→ session-filtered slot fetch
// Nigerian default calendar
getCurrentAcademicSession(); // e.g. "2025/2026" — Aug+ = new session
getCurrentSemester(); // 1 = Aug–Jan, 2 = Feb–Jul
getAcademicContext(); // { academic_session, semester }Defined in:
uniflow-web/src/lib/academic.tsuniflow-app/lib/academic.ts
All new reads/writes should filter by both fields.
- Create lecturers in uni portal with emails matching CSV.
- Import students CSV for a department + level.
- Go to Departments → Timetable.
- Use Combined import (recommended) → download template → fill → upload.
- Preview must show 0 errors → Confirm & auto-enroll students.
- Sign in as student on mobile → Courses + Timetable should populate.
API contract:
POST /api/timetable/import
{
"university_id": "uuid",
"department_id": "uuid",
"csv_text": "...",
"mode": "preview" | "commit",
"auto_enroll": true
}| Item | Notes |
|---|---|
lecturer_courses |
Still synced on import for legacy mobile/web paths. Remove in phase 2. |
timetable.course_id + lecturer_id |
Still written on import. Mobile falls back if course_offering_id missing. |
enrollments.course_id |
Still written alongside course_offering_id. |
| Old timetable CSV (generate → download → import) | Still on timetable page; combined import is preferred. |
u/courses lecturer assignment UI |
Still present; should be removed when offerings page ships. |
# Web production build
cd uniflow-web && npm run build
# Mobile (no full build required for dev)
cd uniflow-app && npx expo export --platform android # optional- Run SQL migrations in Supabase (blocker for offerings in prod).
- Test combined CSV import end-to-end on a dev university.
- Build manual enrollment override UI (
POST /api/enrollments). - Simplify
u/coursespage (catalog only). - Update
docs/uniflow-workflow.mdcourses section. - Phase 2 cleanup: remove
lecturer_coursesdependencies.
Note: After the initial course offering work, significant mobile-side hardening was required for timetable, courses, dashboards and resource uploads to actually function. See docs/timetable-courses-resources-fixes.md.
Copy-paste this:
You are continuing the Uniflow monorepo (uniflow-web + uniflow-app).
Read first:
- docs/CONTINUATION.md (handoff + what's done/remaining)
- docs/course-offering-design.md (courses/timetable model)
- docs/uniflow-workflow.md (platform workflow)
The Course Offering redesign is partially implemented. SQL migrations in
uniflow-app/supabase/ MUST be run in Supabase before things work in production.
In particular run `class_updates_migration.sql` if you see errors about timetable_id column or missing columns like delay_minutes / schema cache errors (PGRST204). After running, the migration includes a NOTIFY to reload PostgREST schema.
Your tasks:
1. [Specify task from section 10 above]
2. Do not break mobile backward compatibility with lecturer_courses until phase 2.
3. Always filter timetable/enrollments/offerings by academic_session + semester.
4. Run `npm run build` in uniflow-web after changes.
# Find offering usage
rg "course_offerings" uniflow-web uniflow-app
# Find legacy lecturer_courses
rg "lecturer_courses" uniflow-web uniflow-app
# Find enrollment gaps
rg "enrollments" uniflow-web/src/app/apiLast updated: Course Offering redesign — implementation phase 1 complete, SQL + UI cleanup pending.