Tiago Fortunato
ProjectsOdysUser Journeys

Professional Sign-Up & Onboarding

How a Brazilian independent professional goes from landing on odys.com.br to having a live public booking page.

Professional Sign-Up & Onboarding

Two phases end-to-end: account creation with email verification, then a multi-step onboarding wizard that captures enough data to render the booking widget. All auth via Supabase; all state persisted via Drizzle.

Phase 1 — Account creation

  1. The professional lands on odys.com.br/register and fills the form.
  2. Client POSTs JSON to /api/auth/register (src/app/api/auth/register/route.ts).
  3. Handler is rate-limited — 5 attempts per rolling hour per IP via getRegisterLimiter() in src/lib/ratelimit.ts (Upstash slidingWindow).
  4. Registration branches on NODE_ENV:
    • Productionsupabase.auth.signUp() with the anon client → Supabase sends a verification email via its own SMTP.
    • DevelopmentadminClient.auth.admin.createUser({ email_confirm: true }) skips the email round-trip for faster iteration.
  5. Response returns requiresEmailConfirmation so the client page renders either "check your inbox" or proceeds.

Phase 2 — Email verification → session

  1. Professional clicks the verification link.
  2. Browser hits /auth/callback (src/app/auth/callback/route.ts).
  3. supabase.auth.exchangeCodeForSession(code) converts the one-time code into a Supabase session.
  4. For type=client signups only, the smart-linking query runs — backfills clients.userId for pre-existing rows whose email matches the new verified user. Gated explicitly on type === "client"; professional-flagged signups skip it. The match is email-only, so a single client can be linked across all professionals they've previously booked with.
  5. This only runs after verification. Running it before (the old design) allowed the C1 account-takeover finding.
  6. /auth/redirect reads user_metadata.type and forwards — professionals → /onboarding, clients → /c.

The smart-linking placement after verification is the fix for audit finding C1. See Security → Fixed.

Phase 3 — Onboarding wizard (professionals only)

The /onboarding wizard collects 7 fields, posted as JSON to /api/onboarding:

  • Full name — used to derive the public URL slug
  • Profession — one of 29 keys from src/lib/professions.ts
  • WhatsApp (body key: phone) — canonicalized for reply-routing
  • Session duration — minutes (integer)
  • Session price — cents (integer)
  • Short bio — free-form text, optional (the only nullable field)
  • Weekly availability — array of { dayOfWeek, startTime, endTime } tuples

Handler — /api/onboarding

  1. Rate-limit — 3 submissions per rolling hour per IP (getOnboardingLimiter()).
  2. Zod validationonboardingSchema validates: name (string min 2), profession (string), phone (string min 10), bio (optional), sessionDuration (positive int ≤ 480), sessionPrice (non-negative int), availability (array with dayOfWeek 0-6 and HH:mm start/end). Failure → 400 with field errors.
  3. IdentitygetUser() returns the authenticated Supabase user.
  4. Idempotency — if a professionals row already exists for this user.id, return it without recreating.
  5. Slug generationgenerateUniqueSlug(toSlug(body.name)):
    • toSlug() normalizes NFD → strips diacritics → lowercase → dash-separated → drops non-alphanumerics
    • generateUniqueSlug() blocks reserved top-level routes (api, auth, dashboard, login, register, onboarding, explore, c, p, privacidade, termos, admin, monitoring, sitemap, robots, opengraph-image) — if a professional's name slugs to one of these, the base is skipped and iteration starts at -2. Otherwise, tries the base; on collision, appends -2, -3, -4, … with one DB round-trip per iteration
  6. Insert professional — one row.
  7. Insert availability — one row per weekday window.
  8. Set user metadata{ type: "professional" } so future logins route to the dashboard.

Outcome

Professional is live at odys.com.br/p/[slug].

Why this shape

  • Two phases (account → onboarding) mirrors the pattern where initial signup is lightweight but the first-class data model needs structured input. Collecting everything at once kills conversion.
  • Idempotent onboarding handler protects against double-click / slow-network duplicate submissions.
  • Slug generation as a pure function keeps URL ownership testable and predictable. Professionals don't pick slugs manually — bounded edge case count.
  • Smart-linking after email verification, not before is the direct lesson from audit finding C1. Always re-read the threat model when combining signup with auto-link.
  • Slug-collision loop is intentionally simple — one DB query per retry. At MVP scale the cost is negligible. If thousands of pros ever collide on the same base, swap for a batch query or a dedicated counter column.

On this page