User journey · verified · signup / first use / family setup

Signup + First Use

From "I want to try FuzzyCode" to a parent actively using it with a child — what screens does a user actually see, in what order?

Last verified 2026-04-18 against code
Slug signup-journey
UX reality single-shell + 3 hard redirects
Screen total 11 panels across 5 templates + 1 Stripe

§1 The big idea the UI doesn't tell you

The URL /user_login is not the login page. It's one template (user_management.html, 3603 lines) that swaps between eight hidden panels via a client-side switchForm() helper. What looks to the user like six different screens — sign in, OTP, signup with password, signup OTP, password reset, enter code, set new password, confirmation, welcome — all share the same URL. Browser back / forward / refresh always return the Sign-In panel.

Real route transitions (the 3 that do change the URL): after a successful authenticated signup/login, the user is navigated to one of: /user_profile (complete profile) · /payment-required (consent + plan picker; this is also the COPPA direct notice surface) · /parents/children/manage (family dashboard). Also Stripe's /payment-success and /payment-cancelled (hosted by us) after Stripe's own interstitial.

§2 Parent signup — two UX paths

Signup forks based on the user's link click AND on a backend feature flag, with no visual signal of which path they're on.

Path A — email + password
/user_login #auth-container
Sign In
  • Click link Sign Up
switchForm('signup')
/user_login #signup-container
Sign Up
  • email
  • password
  • ☐ I am 18+ and parent/guardian
  • [Turnstile widget]
  • Sign Up
POST /signup
OR
Path B — email only (OTP)
/user_login #signup-container
Sign Up
  • Sign Up Just Email
switchForm('signup-otp')
/user_login #signup-otp-container
Sign Up (OTP)
  • email
  • ☐ Terms · [Turnstile]
  • Sign Up
POST /request-otp
create_user=true
/user_login #otp-container
Enter OTP
  • 6-digit
  • Verify

After authenticated signup — the real URL transitions

/user_login welcome
Welcome
  • tokens issued
  • auto-redirects
window.location
/user_profile
Complete Your Profile
  • first name last name
  • child name child birthday
  • username
submit
/payment-required
Parent Consent + Plan
  • Yearly / Monthly
  • COPPA direct-notice text (inline, not modal)
  • Checkout → Stripe
Stripe
/payment-success
✓ Success
  • webhook records parental_consent_date
  • auto-redirects to manage
nav
/parents/children/manage
Family Dashboard
  • Household overview
  • Add a child form
  • Child cards
  • "Manage Children" was the entry

§3 Parent adds a child

Every credential mutation requires a parent step-up modal — a password re-auth that mints a short-lived step-up cookie. This is enforced even for the parent who is logged in milliseconds ago.

/parents/children/manage
Family Dashboard
  • Add a child form:
  • private_child_label
  • public_username
  • child_birthday
  • ☐ confirm_private_use
  • Add child
POST /api/parents/children
/parents/children/manage
Family Dashboard
  • new child card appears
  • status: pending_private_use
  • Login Management nav
click Login Management
/parents/children/credentials
Child Credentials
  • Per-child card:
  • login handle
  • new password
  • Save Revoke sessions
click Save
step-up ok
/parents/children/credentials
Child Credentials
  • Save ✓ saved
  • Backend: creates Supabase alias <handle>@email-alias.fuzzycode.dev

§4 Child first login (uses the same /user_login)

There is no child-specific login page. The field placeholder tells both parents and children to use the same input.

👧 Child actor uses handle given by parent
/user_login #auth-container
Sign In
  • Parent Email, Parent Username, or Child Login Handle
  • password
  • Sign In
  • ⚠ Reset / Sign Up / OTP links all visible to child — will all fail
POST /auth/session/login/password
server
Supabase edge fn
  • edge fn sign-in translates handle → alias email
  • broker cookies issued
  • COPPA gates skipped (parent already consented)
nav
/ (editor)
FuzzyCode editor
  • index.html
  • no context banner for child

§5 Password reset — parent only

Four panels of the same shell, one URL, with a strict enforcement that children cannot enter this flow.

/user_login #auth-container
Sign In
  • Reset Password
switchForm('reset')
/user_login #reset-container
Reset Password
  • email
  • Send Reset Code
  • rejects @email-alias.fuzzycode.dev
POST /auth/session/password/reset/request
→ Supabase /auth/v1/recover
type code
/user_login #reset-code-container
Enter Reset Code
  • 6-digit
  • Verify
POST /auth/session/otp/verify
type=recovery
/user_login #update-password-container
Set New Password
  • new password
  • Update Password
  • rejects actor_type=="child"
Child password reset = does not exist. A child cannot self-service reset. The only way to rotate is the parent flow in §3. If a child's password is lost, the parent resets via /parents/children/credentials with step-up.

§6 Account recovery (lost email / locked account)

Beyond password reset, recovery is an out-of-band operator process. The self-service surface is a contact form.

/parents/contact
Parent Contact
  • request type
  • parent email / name
  • child selector / reference
  • message
  • Submit
POST /api/parents/
contact/requests
admin/parent_rights_requests
Admin queue
  • admin runs playbook
  • verifies identity
  • approves / rejects
execute

There is no SMS recovery, no security questions, no recovery codes, and no "trusted device" recovery. Full inventory of what exists: this form + admin playbook.

§7 Parent ↔ child context switch

Two identical-looking switch UIs coexist on the same page and no in-editor switcher exists. A parent who wants to "use as child" must navigate to Family Dashboard, click, then navigate back to the editor.

/parents/children/manage
Family Dashboard
  • Selection panel:
  • child-context-select
  • Use selected child (dropdown path)
  • -- or --
  • Per-card Use this child (card path)
  • Both call selectChildContext(id)
POST /auth/session/
child-context
/parents/children/manage
Family Dashboard
  • (no page reload)
  • text in #selection-summary updates
  • ⚠ no toast; no nav; easy to miss
  • parent step-up cookie cleared
user navigates
/ (editor)
FuzzyCode editor
  • new fc_parent_child_ctx cookie applied
  • ⚠ no context banner
  • parent sees child's drafts (IndexedDB partitioned per-child)

§8 Gotchas the code revealed

OAuth is dead in the UI, alive in the server. The "Sign in with Google" block in user_management.html:664-672 is commented out with a warning not to re-enable. The routes /auth/session/oauth/start + callback are fully wired. A future re-enable needs UAT verification per the comment.
Child sees password-reset and sign-up links they cannot use. The same #auth-container is shown; links don't filter based on input. A child who types their handle and clicks "Reset Password" will fail with an obscure internal error.
Silent signup UX branch. Whether you see an auto-redirect or a "Check Your Email" panel depends on broker_bootstrap_flows_enabled() — a feature flag the user can't see. Two different UX paths for the same action.
/user_profile child fallthrough. A child actor without a name lands on the parent's "Complete Your Profile" page (user_profile.html:222-264) showing parent-only fields. Narrow edge but surfaced by the code.
Context-switch success is nearly invisible. No toast, no page nav. Only the text in #selection-summary changes. Users can easily assume the click did nothing.
Deep-link into /user_profile while logged out silently loses the target. Redirects to /user_login with no ?next= preservation.

§9 Verification pointers