- Introduction
- Pitch
- Hogwarts
- Live Demo
- MVP
- Roadmap
- Launch Sprint
- PRD
- Get Started
- Localhost
- Architecture
- Structure
- Pattern
- Page
- Layout
- Content
- Types
- Config
- Actions
- Queries
- Authorization
- Validation
- Form
- Table
- Detail
- Card
- Util
- Hooks
- List Params
- Views
- README.md
- ISSUE.md
- Technology Stack
- Database
- File
- CDN Assets
- Entry Points
- Dashboard
- Authentication
- Credentials
- OAuth
- Flow Diagrams
- Multi-Tenancy
- Offline
- Onboarding
- Onboarding Videos
- Add Values
- Admission
- Application
- Attendance
- Compliance
- Profile
- Exams
- Exam Wizard
- Timetable
- Classrooms
- Notifications
- Conference
- LMS (Lumos)
- Finance
- Fee Management
- Invoice
- Wallet
- Salary
- Payroll
- Timesheet
- Expenses
- Budget
- Receipt
- Accounts
- Banking
- Reports
- Dashboard
- Permissions
- Messages
- Integration Flow
- Provision
- AI Document Processing
- Document Intelligence
- Internationalization
- Translation
- Translation Guide
- Icons
- Docs Factory
- Inspiration
- Listings
- Teachers
- Students
- Catalog
- Library
- Contributing
- Code of conduct
- GitHub Workflow
- Database Seeds
- Database Safety
- Test Accounts
- Playwright
- Prettier
- Block Rebound
The onboarding system guides new schools through registration, configuration, and business setup. Mirror pattern, ~166 files, 16 active steps in 3 groups, driven by WizardLayout. Typical completion: 10–30 minutes.
Steps
Group 1 — Basic information
| Step | Path | Required | Database |
|---|---|---|---|
| About school | /about-school | No | static welcome |
| Title | /title | Yes | School.name |
| Description | /description | Yes | schoolType, schoolLevel, planType |
| Location | /location | Yes | address, city, state, country |
| Stand out | /stand-out | No | static |
Group 2 — School setup
| Step | Path | Required | Database |
|---|---|---|---|
| Capacity | /capacity | Yes | maxStudents, maxTeachers, maxClasses |
| Branding | /branding | No | primaryColor, borderRadius, shadow |
| Import | /import | No | bulk insert (CSV / Excel) |
| Finish setup | /finish-setup | No | static review |
The schedule step is defined in OnboardingStep with its own component directory but is not in active navigation. Lives at order: 5.5 in config.ts for future activation.
Group 3 — Business and legal
| Step | Path | Required | Database |
|---|---|---|---|
| Join | /join | Yes | User |
| Visibility | /visibility | No | School.isPublished |
| Price | /price | Yes | tuitionFee, registrationFee, applicationFee, currency |
| Discount | /discount | No | Discount model |
| Legal | /legal | Yes | LegalConsent |
| Subdomain | /subdomain | No | School.domain |
After all steps the user lands on /congratulations for a smart redirect (school subdomain dashboard or main domain depending on session state).
Active step order is ONBOARDING_CONFIG in src/components/form/footer.tsx. Authoritative metadata (labels, validation, group, order) is ONBOARDING_STEPS in src/components/onboarding/config.ts.
Auth handshake
Four-stage flow:
- Start — Unauthenticated user visits
/get-startedor/onboarding.start-onboardingaction persists intent in a cookie and redirects to/login. - Authenticate — Standard login. Callback URL routes back to
/onboarding. - Provision —
initializeSchoolSetup()callsensureUserSchool()(src/lib/school-access.ts) — atomicdb.$transactioncreates theSchoolrow,SchoolMember(binds user as ADMIN), seeds defaultYearLevels. Idempotent. - Refresh + redirect —
_sessionRefreshRequired: truetriggers client refetch of/api/auth/session;_redirectsends to next step.
Ownership enforced by requireSchoolOwnership(id) on every step's load. Cross-tenant access raises TenantError.
Multi-tenant integration
- Subdomain routing —
/onboardingand children live on a main domain (ed.databayt.orgorbalqalam.com), not on a subdomain. Subdomain assignment happens at the Subdomain step, and the suffix shown there follows the root the visitor actually arrived on (rootDomainFromLocation()), so a school signing up via balqalam.com is quoted a balqalam.com URL. - Locale in step URLs — every onboarding URL must carry its
[lang]segment. A bare/onboarding/...path is treated as locale-less byproxy.ts, which then re-derives the locale from theNEXT_LOCALEcookie orAccept-Languageheader rather than the page the user was on — flipping Arabic users to English mid-wizard.FormFooternavigates correctly on its own; steps overridingonNextshould save only, or prefixparams.langthemselves. - Database scoping — every read uses
requireSchoolOwnership(id)first, then queries scoped byschoolId. - Session schoolId — JWT carries
schoolIdafter step 1; subsequent steps trust it after re-verification.
Validation
Each step has a Zod schema co-located with its component. Schemas use the i18n factory pattern — same schema produces Arabic or English error messages depending on the dictionary slice.
import { createTitleSchema } from "@/components/onboarding/title/validation"
const schema = createTitleSchema(dictionary)
const result = schema.safeParse(input)Root validation.ts exports validateStep(step, data, dictionary) and getRequiredFieldsForStep(step) for shared step-aware validation.
Auto-provisioning
When completeOnboarding() runs at the legal step, the school is marked active and a 5-step provisioning chain creates academic infrastructure. Each step is wrapped in try/catch so one failure doesn't block the rest.
Source: src/components/onboarding/legal/actions.ts → src/components/catalog/setup.ts.
| Step | Creates |
|---|---|
setupDefaultsForSchool() | YearLevels (8 / 6 / 14 by school level), 6 Departments, 9 ScoreRanges |
setupCatalogForSchool() | Subject / chapter / lesson bridges from the global catalog |
setupTermsForSchool() | Default terms for the academic year |
setupRolesForSchool() | Permission templates for the 8 roles |
setupNotificationsForSchool() | Default notification preferences for ADMIN role |
setupCatalogForSchool resolves curriculum via inferCurriculum():
| Priority | Strategy | Example |
|---|---|---|
| 1 | International school type → US | any country + international |
| 2 | Country exact match | US → US, SD → SD |
| 3 | Unmapped country → US | JP, KE, … |
API routes
| Path | Method | Purpose |
|---|---|---|
/api/onboarding/create-school | POST | Idempotent atomic school creation; handles P2002 unique violation race |
/api/onboarding/validate-access | POST | Pre-flight access check; auto-provisions; returns redirect on mismatch |
/api/onboarding/extract | POST | AI document extraction (10 MB max; JPEG / PNG / WebP / PDF) |
Server actions
In src/components/onboarding/actions.ts:
| Action | Purpose |
|---|---|
initializeSchoolSetup() | Creates or retrieves school for current user; returns _redirect |
getListing(id) | Fetch school with ownership verification |
updateListing(id, data) | Update fields for a step |
getSchoolSetupStatus(schoolId) | Completion percentage (6 checks) |
reserveSubdomainForSchool(id, sub) | Reserve subdomain (delegates to src/lib/subdomain-actions.ts) |
getCurrentUserSchool() | Returns current user's schoolId |
getUserSchools() | Lists schools owned by or associated with current user |
proceedToTitle(schoolId) | Validates ownership and redirects to about-school step |
Key files
| Path | Purpose |
|---|---|
src/app/[lang]/onboarding/[id]/[step]/page.tsx | Each step page (16 routes) |
src/components/onboarding/actions.ts | Server actions |
src/components/onboarding/config.ts | Step config (authoritative) |
src/components/onboarding/types.ts | OnboardingStep, OnboardingSchoolData |
src/components/onboarding/validation.ts | Root Zod schemas |
src/components/onboarding/use-listing.tsx | ListingProvider context |
src/components/onboarding/use-onboarding.ts | Navigation + validation hook |
src/components/onboarding/auth-helpers.ts | getAuthContext, requireSchoolOwnership |
src/components/form/footer.tsx | FormFooter + ONBOARDING_CONFIG |
src/lib/school-access.ts | ensureUserSchool() atomic transaction |
src/components/catalog/setup.ts | Provisioning chain |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Build hangs at "Environments: .env" | TypeScript errors in onboarding components | Run pnpm tsc --noEmit |
Session missing schoolId | Session not refreshed after atomic transaction | Fetch /api/auth/session after _sessionRefreshRequired: true |
| Cross-tenant access denied | User accessing a school they don't own | requireSchoolOwnership(id) throws TenantError |
| Subdomain already taken | Another school reserved it | Action returns errorCode: "DOMAIN_TAKEN" |
| Form data not persisting | Missing revalidatePath() after mutation | All update actions call revalidatePath("/onboarding") |
| Dictionary property not found | Missing translation key | Use optional chaining: dictionary?.onboarding?.title?.label |
See also
- Multi-tenancy — auth handshake, tenant context
- Pattern — mirror pattern, file conventions
- Authentication — login flow