User Signup

Feature Owner: scorevi (Sean Patrick Caintic)
Module: Auth & Security
Priority: P0
Sprint #12: Fully Implemented
Date: 2026-06-29


EXECUTIVE SUMMARY

What is this feature?
Clerk-powered user registration with automatic Supabase sync via webhook (/api/clerk/user-created) and a fallback sync mechanism in authenticateUserWithRole(). New users default to the LEARNER role. A welcome email is sent after successful sync.

Why does it matter?
Without signup, there are no users. Every new user must be persisted in both Clerk (identity) and Supabase (role + app data) with consistency guarantees. A failed sync means a user can authenticate but has no role — causing 500 errors across the app.

What's the MVP scope?
Clerk SignUp component at /sign-up. Webhook handler at /api/clerk/user-created (109 lines) that validates via Zod, syncs via syncUser(), and sends welcome email. Fallback sync in authenticate.ts with retry logic. Webhook handler at /api/clerk/user-updated for profile updates.


1. USER PAIN POINT & SOLUTION

Current State (Without Feature)

No way to create an account. Platform requires manual user provisioning by an admin via Clerk dashboard and Supabase SQL insert.

Pain Point

Emotional: Platform feels exclusive and unapproachable. Users cannot self-serve.
Functional: Manual provisioning is slow, error-prone, and creates a bottleneck at admin.
Business Impact: Cannot scale user acquisition. Every new user costs admin time.

Future State (With Feature)

Users self-register via Clerk SignUp component. Webhook automatically provisions their record in Supabase with LEARNER role. Welcome email confirms successful registration.

Marketing Hook

"Sign up in seconds. Start learning immediately."


2. 4D FRAMEWORK MAPPING

Diagnose

N/A — auth infrastructure.

Design

Only registered creators can design quests. Role is set at registration (or later by admin).

Develop

Only registered learners can enroll in quests and track progress.

Deliver

Registered users can receive SCORM export links and share quests.


3. USER FLOWS

Entry Point

User clicks "Sign Up" on the landing page or sign-in page, navigates to /sign-up.

Success Criteria

Clerk account created → Supabase record upserted with LEARNER role → welcome email sent → user redirected to /redirect-check → routed to learner dashboard.

Main Flow (Happy Path)

  1. User fills Clerk SignUp form at /sign-up.

  2. Clerk creates identity and fires user.created webhook to /api/clerk/user-created.

  3. Webhook validates payload via clerkUserWebhookSchema (Zod).

  4. syncUser() upserts into app_users: clerk_id, email, name, role: 'LEARNER' (on conflict clerk_id DO UPDATE). Returns agency_id.

  5. Welcome email is sent via EmailService.

Edge Cases

  • Webhook delayed/failed: authenticateUserWithRole() in authenticate.ts calls syncUserIfNotExists() as fallback on first sign-in. Retries currentUser() up to 3 times with 500ms×attempt backoff.

  • Webhook payload invalid: ApiResponseHelper.validationError() with Zod fieldErrors.

  • Email send fails: Returns 201 success with { message: "Learner added but email failed" } — user sync still completed.

  • Sign-up page when already signed in: forceRedirectUrl="/redirect-check" redirects authenticated users.

  • Rate limiting: Handled by Clerk — no custom rate limiting.

Decision Points

  • IF Clerk webhook fires successfully → sync user via webhook path (faster).

  • ELSE (webhook missed) → sync user on first auth check via syncUserIfNotExists() fallback.

  • IF email send succeeds → 201 with "User synced successfully".

  • ELSE → 201 with email failure details (non-blocking).


4. INFORMATION ARCHITECTURE

Primary Information (Always visible)

  • Email (required).

  • Password (required, Clerk-enforced strength).

  • Name (first + last, from Clerk SignUp component).

Secondary Information

  • Welcome email sent to registered email address.

Actions

Primary CTA: "Create Account" (Clerk SignUp component).
Secondary Actions: "Already have an account? Sign In" link.


5. WIREFRAMES

[Excluded — existing UI: app/(main)/sign-up/[[...sign-up]]/page.tsx]

6. WIREFLOWS

Excluded.

7. PROTOTYPE

[Excluded — existing implementation]


8. BACKEND SCHEMA

Database Tables

app_users (written by syncUser):

-- syncUser upserts these fields (NOT profile_image_url):
-- clerk_id: TEXT (from Clerk user ID)
-- email: TEXT
-- name: TEXT (first_name + last_name)
-- role: 'LEARNER' (default)
-- On conflict (clerk_id): DO UPDATE (preserves existing role if re-syncing)

Webhook Schema (Zod)

const clerkUserWebhookSchema = z.object({
data: z.object({
id: z.string().min(1),
email_addresses: z.array(z.object({ email_address: z.string().email() })).min(1),
first_name: z.string().nullable().optional(),
last_name: z.string().nullable().optional(),
image_url: z.string().nullish(),
public_metadata: z.record(z.unknown()).optional(),
private_metadata: z.record(z.unknown()).optional(),
}),
type: z.string().optional(),
});

Note: Webhook schema WRAPS fields in data.z.object({...}) — Clerk's webhook payloads nest user data inside a data key.


9. API ENDPOINTS

Method

Endpoint

Auth

File

Description

POST

/api/clerk/user-created

Clerk webhook secret

app/api/clerk/user-created/route.ts (109 lines)

Zod validates payload, calls syncUser(), sends welcome email

POST

/api/clerk/user-updated

Clerk webhook secret

app/api/clerk/user-updated/route.ts

Updates email, name, profile_image_url on change


10. DATA REQUIREMENTS

Frontend Needs

  • Clerk SignUp component (@clerk/nextjs).

  • forceRedirectUrl="/redirect-check" for post-signup routing.

API Calls Frontend Will Make

  • Clerk SignUp component handles all auth API calls internally. No custom API calls from the signup page.

Caching Strategy

  • roleCache (30s TTL) in authenticate.ts caches freshly synced user on first auth check.

  • userIdCache (5 min TTL) caches Clerk→Supabase ID mapping.


11. PERFORMANCE CONSIDERATIONS

Database Optimization

  • syncUser uses upsert with onConflict: 'clerk_id' — atomic, single query.

  • syncUserIfNotExists (fallback) does: select existing → conditionally upsert → select final. Three queries, but only on first sign-in.

API Response Time

  • Webhook handler: <500ms (one Supabase upsert + optional email send).

  • Fallback sync: <2s (includes Clerk currentUser() call and retries).


12. SECURITY & AUTHORIZATION

Who can access this feature?

Anyone. Public route.

Authorization Logic

  • middleware.ts: /sign-up(.*) and /api/clerk(.*) are public routes.

  • Clerk webhook should be verified via Svix signature header (Clerk best practice).

Data Validation

  • Webhook payload validated via clerkUserWebhookSchema before any DB write.

  • syncUser validates clerkId and email are present before upsert.


13. ERROR HANDLING

Error

Response

Invalid webhook payload

400 ApiResponseHelper.validationError() with Zod field errors

syncUser DB error

{ error: error.message } returned → webhook returns 500 ApiResponseHelper.internalError()

Email send fails

201 success with email error details (user sync succeeded)

currentUser() null (retry exhausted)

AuthenticationError "User not found"

Duplicate registration (same Clerk ID)

onConflict: 'clerk_id' DO UPDATE — idempotent


14. TESTING CHECKLIST

Happy Path
□ New user signs up via Clerk → webhook fires → Supabase record created with LEARNER role → welcome email sent → redirected to learner dashboard.
□ Login after signup → authenticateUserWithRole() returns cached role, no re-sync needed.

Edge Cases
□ Webhook not received → fallback syncUserIfNotExists() creates record on first auth check.
currentUser() returns null (OAuth race condition) → retry 3 times succeeds.
□ Email send failure → user still created, email error returned in 201 response.
□ Duplicate Clerk webhook event → idempotent upsert (onConflict clause).
□ Signup page visited while signed in → redirected to /redirect-check.


15. OPEN QUESTIONS

  • Should webhook signature verification (Svix) be implemented for production security?

  • Should profile_image_url be synced in the initial syncUser() call (currently only synced on user-updated)?

  • Should new users be assigned to a default agency?


16. OUT OF SCOPE (v1.1+)

  • Invite-only registration (agency-controlled signup).

  • Custom registration fields beyond Clerk defaults.

  • Email verification flow customization.


17. SUCCESS METRICS

  • 100% of Clerk registrations result in a valid Supabase record (via webhook OR fallback).

  • <5s from signup to dashboard (including redirect-check polling).

  • 0 orphaned Clerk accounts (no Supabase record) after 24h.


18. DEPENDENCIES

This feature depends on:

  • Clerk SignUp component and webhook infrastructure.

  • lib/syncUser.ts (40 lines): upserts clerk_id, email, name, role (NOT profile_image_url).

  • lib/auth/authenticate.ts (885 lines): syncUserIfNotExists() fallback, retry logic.

  • lib/email/emailService.ts for welcome email.

  • redirect-check/page.tsx (169 lines) for post-signup role routing.

These features depend on this:

  • All authenticated features (0.1 Secure Login prerequisite).

  • Role-based dashboards.


19. TIMELINE & OWNERSHIP

  • Implemented: Sprint 0 (foundational).

  • Owner: scorevi (Sean Patrick Caintic)


Document Version

1.0 - Initial version - 2026-06-29 08:05 UTC

1.1 - Added Document Version section and update author to have full name - 2026-06-29 08:40 UTC

1.1.1 (Current Version) - Minor edits - 2026-06-29 08:41 UTC


Was this article helpful?