Tenant Onboarding
Use this page when owner sign-up succeeds but the user is stuck on /onboarding, when the onboarding wizard refuses a tenant address, or when the final completion step returns 403, 404, 409, or a custom-domain resolution failure.
This page is a troubleshooting runbook for the current owner onboarding flow. It is not the old schema-provisioning model.
Source Surfaces
| Surface | Source path |
|---|---|
| Owner registration endpoint | lumie-backend/modules/auth/src/main/java/com/lumie/auth/adapter/in/web/AuthController.java |
| Owner registration service | lumie-backend/modules/auth/src/main/java/com/lumie/auth/application/service/AuthRegistrationService.java |
| Tenant auto-registration | lumie-backend/modules/tenant/src/main/java/com/lumie/tenant/application/service/TenantRegistrationService.java |
| Tenant onboarding command | lumie-backend/modules/tenant/src/main/java/com/lumie/tenant/application/service/TenantCommandService.java |
| Tenant public lookup | lumie-backend/modules/tenant/src/main/java/com/lumie/tenant/application/service/TenantQueryService.java |
| Tenant entity rules | lumie-backend/modules/tenant/src/main/java/com/lumie/tenant/domain/entity/Tenant.java |
| Frontend onboarding gate | lumie-frontend/src/entities/session/model/onboarding.ts |
| Frontend onboarding layout | lumie-frontend/app/(onboarding)/layout.tsx |
| Onboarding wizard | lumie-frontend/src/features/onboarding/ui/OnboardingWizard.tsx |
| Custom ID step | lumie-frontend/src/features/onboarding/ui/steps/CustomIdStep.tsx |
| Tenant API query wrappers | lumie-frontend/src/entities/tenant/api/queries.ts |
| Tenant redirect logic | lumie-frontend/src/shared/lib/tenantRedirect.ts |
Frontend /api proxy and custom-domain resolution | lumie-frontend/app/api/[...path]/route.ts |
Symptoms
| Symptom | Likely cause |
|---|---|
Owner always lands on /onboarding after login | tenantOnboardingCompletedAt is still null in the session or the completion call never succeeded |
/onboarding immediately redirects to /admin | onboarding was already completed, so the layout gate is working as designed |
| Custom ID looks free but the wizard says it is unavailable | the public lookup returned 200, the value is reserved, the pattern is invalid, or the availability request failed and the UI treated that as unavailable |
Final submit returns 403 | request path slug and authenticated tenant context do not match, triggering TENANT_019 |
Final submit returns 409 | onboarding was already completed (TENANT_018) or the requested customId is already taken |
Custom-domain login or onboarding submit returns 503 from the frontend proxy | the frontend could not resolve the tenant for that host before forwarding the auth write |
Identifier Map
The onboarding flow uses three tenant-facing identifiers with different jobs:
| Identifier | Example | Owner | Purpose |
|---|---|---|---|
| tenant slug | inst-a1b2c3d4 | backend | authenticated tenant context and internal routing |
| custom ID | a1b2c3d4 or myacademy | backend plus frontend | public lumie-edu.com/{customId} path and onboarding address selection |
| custom domain | academy.example.com | frontend plus backend | optional host-based public entry |
Tenant.createWithAutoSlug(...) creates the owner tenant with an inst-<uuid8> slug and a default customId derived from that slug. The first user-set public address can be applied during onboarding, or later through the regular tenant update path while the tenant still has the default slug-derived value.
Current Runtime Contract
1. Owner sign-up creates an active tenant immediately
AuthRegistrationService.registerOwner(...) calls TenantService.createTenant(...), which delegates to TenantRegistrationService.registerTenant(...).
That service:
- inserts the tenant row
- moves it through
PENDING -> PROVISIONING -> ACTIVE - publishes
TenantCreatedEventafter commit
There is no runtime Flyway or per-tenant schema provisioning in the current flow. A successful owner sign-up should already have an active tenant before the onboarding wizard opens.
2. The frontend gate is role plus timestamp based
needsTenantOnboarding(user) is:
return user.role === 'OWNER' && user.tenantOnboardingCompletedAt === null;
That means:
- only
OWNERusers are sent to/onboarding STUDENTusers are redirected to/dashboard- non-student users with a non-null onboarding timestamp are redirected to
/admin
3. The completion endpoint is owner-only and tenant-bound
The wizard submits to POST /v1/tenants/{slug}/complete-onboarding.
TenantCommandService.completeOnboarding(...) requires:
- authenticated
OWNER TenantContextHolder.getTenant()matches the{slug}path- optional
customIdpasses format and reserved-word rules - requested
customIdis not already taken onboardingCompletedAtis stillnull
On success it updates phone, optional first-set customId, hidden sidebar items, and onboardingCompletedAt.
4. Public custom ID availability is 404 means free
The frontend availability check calls GET /api/v1/tenants/public/by-custom-id/{customId}.
The UI semantics are:
200 OK: an active tenant already owns thatcustomId, so it is unavailable404 Not Found: no active tenant owns it, so it is available- network or proxy failure: treated as unavailable
5. Custom-domain auth writes fail closed
For /api/v1/login and /api/v1/register, app/api/[...path]/route.ts resolves the tenant from the host when the request comes from a custom domain. If resolution fails, the proxy returns 503 instead of forwarding a tenantless write upstream.
Diagnostic Path
- Identify where the failure happens: owner registration, login redirect, custom ID availability, completion submit, or post-completion redirect.
- Inspect the authenticated user payload from
GET /api/v1/mein the browser network panel. Confirmrole,tenantSlug,tenantCustomId,tenantCustomDomain, andtenantOnboardingCompletedAt. - If the owner is stuck on
/onboarding, confirmtenantOnboardingCompletedAtis stillnull. If it is not null, the problem is stale client state or a redirect path, not incomplete onboarding. - If the custom ID check fails, validate the exact rules: lowercase letters, numbers, or hyphen only; length 3 to 30; not a reserved top-level route such as
admin,api,auth,dashboard,pricing, or_next. - If the completion submit returns
403, compare the request path slug with the authenticated tenant slug.TenantCommandServicerejects mismatches withTENANT_019. - If the completion submit returns
409, distinguish betweenTENANT_018already-completed onboarding and a duplicatecustomIdconflict. - If a custom domain is involved, inspect the frontend proxy response first. A
503from/api/v1/loginor/api/v1/registerusually means tenant resolution failed before the backend saw the request.
Fix
- If onboarding is already complete, refresh the session state. The frontend wizard already treats
TENANT_018as success-by-refresh and redirects to/admin. - If the chosen
customIdis invalid or reserved, choose a new lowercase, public-safe value. The first user-set value becomes immutable after it is saved. - If the owner skipped the custom ID step, the backend keeps the default slug-derived
customIdand still completes onboarding. The first user-set public address can still be applied later while the tenant remains on that default value. - If the submit fails with
TENANT_019, re-enter through a login path that resolves the correct tenant before retrying. Root login works for owners throughpublic.owner_directory; custom-domain or path-based login must resolve the same tenant as the authenticated session. - If custom-domain login returns
503, verify the host resolves throughGET /api/v1/tenants/public/by-domain?host=<host>and that the frontend environment still enables custom-domain resolution.
Verification
Use read-only checks first:
cd /Users/bluemayne/Projects/Lumie
rg -n "needsTenantOnboarding|completeOnboarding|TENANT_018|TENANT_019|getPublicTenantByCustomId|buildTenantLoginUrl" \
lumie-frontend lumie-backend
curl -i "https://lumie-edu.com/api/v1/tenants/public/by-custom-id/<candidate-custom-id>"
curl -i "https://lumie-edu.com/api/v1/tenants/public/by-domain?host=<candidate-host>"
Success signals:
GET /api/v1/meshowsOWNERplus the expected tenant slug- completed tenants show a non-null
tenantOnboardingCompletedAt 404from the publicby-custom-idlookup means the custom ID is free- custom-domain resolution returns a tenant instead of a proxy
503
Prevention
- Treat tenant slug and public custom ID as different identifiers. The onboarding page changes the public one, not the internal slug.
- Keep the frontend reserved custom-ID list and the backend reserved custom-ID list in sync.
- Do not bypass the frontend proxy for custom-domain auth writes; the proxy is the place that injects tenant context from the host.
- When investigating a false onboarding loop, check session freshness before changing backend tenant state.