Onboarding Multi-Step Obrigatório — Final Report
What Was Built
A mandatory 3-step onboarding flow that users must complete before accessing the application. The flow collects personal data (name, username), address (street, city, state, country), and preferences (theme, language, currency, timezone). Users who haven't completed onboarding are automatically redirected to /onboarding from any protected route.
Architecture
Backend (Django)
- New field
display_usernameonCustomUser(apps/users/models.py:64) — CharField(30), not unique at DB level (uniqueness validated in view to allow empty defaults for existing users) - New field
is_onboardedonMembership(apps/communities/models/membership.py:35) — BooleanField, default=False - Updated
SignupProfileUpdateRequestSerializer(apps/headless/schemas.py:1133) — acceptsdisplay_username,address,city,state,country,theme_preference,language,currency,timezone - Updated
CurrentMemberSerializerandProfileSerializer— returnis_onboardedanddisplay_username - Updated
SignupProfileView.put(apps/headless/views.py:3012) — handles all new fields, validates display_username uniqueness, setsmembership.is_onboarded = Trueon completion - Updated
CommunityMemberView.get(apps/headless/views.py:162) — returnsis_onboardedanddisplay_username
Frontend (TanStack SPA)
/onboardingroute (frontend/src/routes/onboarding.tsx) — dark gradient layout, no sidebar/topbar, 3-step wizard- Onboarding components (
frontend/src/components/onboarding/):StepPersonal.tsx— first_name, last_name, display_username with @ prefixStepAddress.tsx— address, city, state, country (select)StepPreferences.tsx— theme (button toggle), language, currency, timezone (selects)OnboardingWizard.tsx— step container with progress bar, navigation, validation
- Onboarding gate in
__root.tsx:beforeLoad—getSession()now returnsis_onboarded; redirects to/onboardingif false and route is not exempt - Exempt routes:
/onboarding,/login,/register,/forgot-password,/confirm-email,/reset-password,/invite,/community-not-found
Data Flow
Register → /signup/profile (name/headline) → /onboarding (3 steps) → /
Login → beforeLoad checks is_onboarded → false → /onboarding → /Design Decisions
display_usernamenot unique at DB level: Existing users all have""as default, which would violate a unique constraint. Uniqueness is enforced in the view layer instead.is_onboardedon Membership, not CustomUser: Onboarding is per-community — a user could be onboarded in one community but not another.- Client-side gate in RootComponent, not server-side beforeLoad: The
beforeLoadruns server-side during SSR wherewindowand community slug headers aren't available. The client-side check inRootComponentis simpler and more reliable. - Dark gradient layout: Onboarding uses a standalone dark theme (
from-gray-950 via-gray-900 to-gray-950) with glassmorphism card, independent of user's theme preference.
Usage
- New user registers → redirected to
/signup/profile→ then to/onboarding - User completes 3 steps (Personal → Address → Preferences) → redirected to
/ - Returning user with
is_onboarded=false→ automatically redirected to/onboarding - User can update preferences later via Settings > Profile
Verification
- Python lint: All modified files pass
ruff check - TypeScript: Compiles with no new errors (pre-existing errors in unrelated files)
- Django tests: headless (40/40 pass), users (2/2 pass), communities (3 pre-existing JWT auth failures unrelated to changes)
- Migrations:
0006_add_display_usernameand0009_add_is_onboardedapplied successfully
Journey Log
- [dead end] Initially set
display_usernameasunique=Truein the model — failed during migration because existing users all have""as default, violating the unique constraint. Removed DB-level uniqueness, validate in view instead. - [lesson] The
simple_history.adminmodule exportsSimpleHistoryAdmin, notHistoryAdmin. Fixed pre-existing import bug incommunities/admin.pyandspaces/admin.pyto unblock migrations. - [pivot] Originally planned server-side gate in
beforeLoad, but SSR doesn't have access towindowor community slug headers. ExtendedgetSession()to returnis_onboardedfrom the existingcommunity_memberAPI call (no extra HTTP request needed).