Skip to main content

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

SurfaceSource path
Owner registration endpointlumie-backend/modules/auth/src/main/java/com/lumie/auth/adapter/in/web/AuthController.java
Owner registration servicelumie-backend/modules/auth/src/main/java/com/lumie/auth/application/service/AuthRegistrationService.java
Tenant auto-registrationlumie-backend/modules/tenant/src/main/java/com/lumie/tenant/application/service/TenantRegistrationService.java
Tenant onboarding commandlumie-backend/modules/tenant/src/main/java/com/lumie/tenant/application/service/TenantCommandService.java
Tenant public lookuplumie-backend/modules/tenant/src/main/java/com/lumie/tenant/application/service/TenantQueryService.java
Tenant entity ruleslumie-backend/modules/tenant/src/main/java/com/lumie/tenant/domain/entity/Tenant.java
Frontend onboarding gatelumie-frontend/src/entities/session/model/onboarding.ts
Frontend onboarding layoutlumie-frontend/app/(onboarding)/layout.tsx
Onboarding wizardlumie-frontend/src/features/onboarding/ui/OnboardingWizard.tsx
Custom ID steplumie-frontend/src/features/onboarding/ui/steps/CustomIdStep.tsx
Tenant API query wrapperslumie-frontend/src/entities/tenant/api/queries.ts
Tenant redirect logiclumie-frontend/src/shared/lib/tenantRedirect.ts
Frontend /api proxy and custom-domain resolutionlumie-frontend/app/api/[...path]/route.ts

Symptoms

SymptomLikely cause
Owner always lands on /onboarding after logintenantOnboardingCompletedAt is still null in the session or the completion call never succeeded
/onboarding immediately redirects to /adminonboarding was already completed, so the layout gate is working as designed
Custom ID looks free but the wizard says it is unavailablethe 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 403request path slug and authenticated tenant context do not match, triggering TENANT_019
Final submit returns 409onboarding was already completed (TENANT_018) or the requested customId is already taken
Custom-domain login or onboarding submit returns 503 from the frontend proxythe 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:

IdentifierExampleOwnerPurpose
tenant sluginst-a1b2c3d4backendauthenticated tenant context and internal routing
custom IDa1b2c3d4 or myacademybackend plus frontendpublic lumie-edu.com/{customId} path and onboarding address selection
custom domainacademy.example.comfrontend plus backendoptional 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 TenantCreatedEvent after 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 OWNER users are sent to /onboarding
  • STUDENT users 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 customId passes format and reserved-word rules
  • requested customId is not already taken
  • onboardingCompletedAt is still null

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 that customId, so it is unavailable
  • 404 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

  1. Identify where the failure happens: owner registration, login redirect, custom ID availability, completion submit, or post-completion redirect.
  2. Inspect the authenticated user payload from GET /api/v1/me in the browser network panel. Confirm role, tenantSlug, tenantCustomId, tenantCustomDomain, and tenantOnboardingCompletedAt.
  3. If the owner is stuck on /onboarding, confirm tenantOnboardingCompletedAt is still null. If it is not null, the problem is stale client state or a redirect path, not incomplete onboarding.
  4. 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.
  5. If the completion submit returns 403, compare the request path slug with the authenticated tenant slug. TenantCommandService rejects mismatches with TENANT_019.
  6. If the completion submit returns 409, distinguish between TENANT_018 already-completed onboarding and a duplicate customId conflict.
  7. If a custom domain is involved, inspect the frontend proxy response first. A 503 from /api/v1/login or /api/v1/register usually 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_018 as success-by-refresh and redirects to /admin.
  • If the chosen customId is 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 customId and 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 through public.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 through GET /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/me shows OWNER plus the expected tenant slug
  • completed tenants show a non-null tenantOnboardingCompletedAt
  • 404 from the public by-custom-id lookup 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.