Professional Onboarding
Professional sign-up + onboarding flow end-to-end
The professional sign-up and onboarding journey is a critical path for new users joining Odys, guiding them from initial account creation to setting up their professional profile and availability. This process involves several API routes and database interactions, ensuring that new professionals can quickly establish their presence and begin managing clients and appointments.
Overview
The journey begins with a user registering an account, typically as a professional. This initial step is handled by the /api/auth/register endpoint, which integrates with Supabase for authentication. Once the user's email is verified, they are redirected through the /auth/callback route, where their user metadata is updated. The final stage is the onboarding process itself, managed by the /api/onboarding endpoint. Here, the professional provides essential details like their name, profession, contact information, session pricing, and availability, which are then persisted in the database. Throughout this flow, rate limiting is applied to prevent abuse and ensure system stability.
The core data structures involved are the professionals table, which stores 24 columns of detailed information about each professional, and the availability table, which captures their working hours across 5 columns. These tables are central to defining a professional's public profile and their capacity to take appointments.
Registration
The /api/auth/register route handles the initial account creation for new users. It accepts POST requests containing an email, password, and optionally a name and user type.
Before processing any registration, the system applies a rate limit using getRegisterLimiter from src/lib/ratelimit.ts. This limiter permits up to 5 registration attempts per hour from a single IP address, returning a 429 Too Many Requests status if exceeded. This helps mitigate brute-force attacks or spam registrations.
Input data is rigorously validated using zod against the registerSchema. This schema mandates a valid email format and a password of at least 8 characters, aligning with Supabase's default authentication requirements. The name must be at least 2 characters long, and type is constrained to an enum of "client" or "professional".
A key design choice here is the conditional use of Supabase clients based on the environment:
- In
production, the standardsupabase.auth.signUp()method is used. This triggers Supabase's built-in email verification flow, sending a confirmation email to the user. The response indicates whether email confirmation is required by checking if a session was immediately returned. - In
development, anadminClient(initialized lazily with theSUPABASE_SERVICE_ROLE_KEY) is used to calladmin.createUser()withemail_confirm: true. This bypasses the email verification step, allowing developers to rapidly test the onboarding flow without needing to manually confirm emails.
Upon successful registration, the route returns a 200 OK response, indicating success and whether email confirmation is pending. Any errors during validation or Supabase interaction result in a 400 Bad Request or 500 Internal Server Error response, with details provided where appropriate.
Authentication Callback
The /auth/callback route is a GET endpoint that Supabase redirects to after a user successfully verifies their email or completes a password recovery flow. It plays a crucial role in finalizing the authentication process and updating user metadata.
The route extracts a code and an optional type parameter from the URL's search parameters. The code is exchanged with Supabase to establish a user session. The type parameter is carefully parsed using the parseType function, which checks against a VALID_TYPES set ("professional", "client", "recovery"). This strict validation is a security measure to prevent an attacker from injecting arbitrary values into the user's metadata via a crafted callback URL.
If the type is "recovery", the user is redirected to the /reset-password page. For "professional" or "client" types, the user's Supabase metadata is updated to reflect their designated role.
A notable feature implemented here is "smart-linking" for clients. If a user registers as a client and has a verified email, the system attempts to link their new userId to any existing clients records that share the same email but currently have a null userId. This uses Drizzle ORM to update the clients table, ensuring that pre-existing client entries (perhaps created by a professional) are correctly associated with a newly registered user account.
Finally, the user is redirected to /auth/redirect on success, or /login?error=auth if the session exchange fails. The origin for redirects is dynamically constructed using x-forwarded-host and x-forwarded-proto headers to ensure correct routing in production environments like Vercel.
Professional Onboarding
After a professional registers and verifies their email, they proceed to the onboarding flow, handled by the /api/onboarding route, which accepts POST requests.
Similar to registration, this route is protected by getOnboardingLimiter, allowing only 3 attempts per hour per IP address to prevent excessive submissions. It also requires an authenticated user, returning a 401 Unauthorized if no user session is found.
The onboarding process is designed to be idempotent: if a professionals record already exists for the authenticated user's userId, the existing record is returned immediately, preventing duplicate entries.
New professional data is validated using zod against the onboardingSchema. This schema ensures that name, profession, and phone are provided, sessionDuration is a positive integer up to 480 minutes, sessionPrice is a non-negative integer, and availability is an array of at least one entry, each specifying a dayOfWeek and valid startTime/endTime strings.
A unique slug is generated for each professional's public profile. The toSlug function normalizes the professional's name, and generateUniqueSlug ensures that the slug is unique within the professionals table and does not conflict with any of the RESERVED_SLUGS (e.g., "api", "auth", "dashboard"). If a conflict occurs, a numeric suffix is appended (e.g., john-doe-2).
Once validated, a new record is inserted into the professionals table, populating its 24 columns with the provided details. If availability data is included in the request, multiple entries are inserted into the availability table, linking them to the newly created professional via professionalId.
Finally, the user's Supabase metadata is updated to set their type to "professional" and their name. In production environments, a welcome email is sent to the professional using sendProfessionalWelcomeEmail. This email sending is a fire-and-forget operation, meaning the API response does not wait for the email to be successfully sent, preventing potential delays in the user experience.
Design Decisions
The design of the sign-up and onboarding flow reflects several deliberate choices aimed at balancing user experience, security, and development efficiency:
- Conditional Supabase Client for Development: The use of
admin.createUserin development environments for/api/auth/registersignificantly accelerates the testing cycle by bypassing email verification. This trade-off prioritizes developer velocity, acknowledging that theSUPABASE_SERVICE_ROLE_KEYis a powerful credential but is confined to non-production settings. - Strict
VALID_TYPESSet: TheVALID_TYPESset insrc/app/auth/callback/route.tsis a critical security measure. By explicitly defining and validating accepted user types from the OAuth callback URL, the system prevents malicious actors from injecting arbitrary data into user metadata, addressing a potential vulnerability. - Comprehensive Slug Generation: The
generateUniqueSlugfunction insrc/app/api/onboarding/route.tsis designed to create user-friendly and unique profile URLs. It handles name normalization, checks against a list ofRESERVED_SLUGSto prevent route conflicts, and automatically appends numeric suffixes to ensure uniqueness. This prevents users from inadvertently shadowing core application routes or creating duplicate profile URLs. - Idempotent Onboarding: The check for an existing
professionalsrecord at the start of the/api/onboardingPOSTrequest makes the operation idempotent. This means that if a user or client-side error causes the request to be sent multiple times, it will not result in duplicate professional profiles, improving data consistency. - Fire-and-Forget Welcome Email: Decoupling the welcome email sending from the main request-response cycle in
/api/onboardingensures that the user's onboarding experience is not delayed by the latency of the email service. While aconsole.warnlogs failures, the primary goal is to provide immediate feedback to the user. - Smart-linking for Clients: The logic in
/auth/callbackto link a newly registered user'suserIdto existingclientsrecords with matching emails andnulluserIds is a user experience enhancement. It automatically reconciles data, ensuring that clients previously managed by a professional can seamlessly transition to having their own user accounts.
Potential Improvements
professionals.emailRedundancy: Theprofessionalstable includes anemailcolumn, which is marked as nullable. However, theuserIdcolumn, which links to the Supabase authentication system, is non-nullable. Since Supabase users are inherently tied to an email address, theprofessionals.emailcolumn might be redundant or could be made non-nullable to reflect that a professional always has an associated email. This would reduce data duplication and potential inconsistencies if the email inprofessionalswere to diverge from the Supabase user's email. (Seeprofessionals.emailinsrc/lib/db/schema.ts)- Dynamic
RESERVED_SLUGSManagement: TheRESERVED_SLUGSset insrc/app/api/onboarding/route.tsis hardcoded. As the application grows and new top-level routes are added, this list will require manual updates. Consider implementing a mechanism to dynamically generate or centralize this list, perhaps by introspecting the application's route configuration, to reduce maintenance overhead and prevent future slug conflicts. (SeeRESERVED_SLUGSinsrc/app/api/onboarding/route.ts) - Enhanced Availability Time Validation: The
onboardingSchemainsrc/app/api/onboarding/route.tsvalidatesstartTimeandendTimeusing a regex^\d{2}:\d{2}$. While this ensures the format, it does not validate the logical correctness of the times (e.g.,startTimemust be beforeendTime) or that they represent actual valid clock times (e.g.,25:00would pass the regex but is invalid). Adding more specific validation logic, such as parsing these strings into time objects and performing comparisons, would improve data quality and prevent invalid availability entries. (Seeavailabilityschema insrc/app/api/onboarding/route.ts) - Robust Welcome Email Error Handling: The
sendProfessionalWelcomeEmailcall insrc/app/api/onboarding/route.tsuses a.catchblock that only logs a warning to the console if the email fails to send. For a critical user onboarding step, a more robust error handling strategy might be beneficial. This could include implementing retries, sending failed email events to a dead-letter queue for later processing, or notifying an administrator, to ensure that all new professionals receive their welcome message. (SeesendProfessionalWelcomeEmailcall insrc/app/api/onboarding/route.ts)
References
src/app/api/auth/register/route.tssrc/app/auth/callback/route.tssrc/app/api/onboarding/route.tssrc/lib/ratelimit.tssrc/lib/db/schema.ts