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)
User fills Clerk SignUp form at
/sign-up.Clerk creates identity and fires
user.createdwebhook to/api/clerk/user-created.Webhook validates payload via
clerkUserWebhookSchema(Zod).syncUser()upserts intoapp_users:clerk_id,email,name,role: 'LEARNER'(on conflictclerk_idDO UPDATE). Returnsagency_id.Welcome email is sent via
EmailService.
Edge Cases
Webhook delayed/failed:
authenticateUserWithRole()inauthenticate.tscallssyncUserIfNotExists()as fallback on first sign-in. RetriescurrentUser()up to 3 times with 500ms×attempt backoff.Webhook payload invalid:
ApiResponseHelper.validationError()with ZodfieldErrors.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 |
| Clerk webhook secret |
| Zod validates payload, calls |
POST |
| Clerk webhook secret |
| Updates |
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) inauthenticate.tscaches freshly synced user on first auth check.userIdCache(5 min TTL) caches Clerk→Supabase ID mapping.
11. PERFORMANCE CONSIDERATIONS
Database Optimization
syncUserusesupsertwithonConflict: '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).
Who can access this feature?
Anyone. Public route.
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
clerkUserWebhookSchemabefore any DB write.syncUservalidatesclerkIdandemailare present before upsert.
13. ERROR HANDLING
Error | Response |
|---|---|
Invalid webhook payload | 400 |
|
|
Email send fails | 201 success with email error details (user sync succeeded) |
|
|
Duplicate registration (same Clerk ID) |
|
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_urlbe synced in the initialsyncUser()call (currently only synced onuser-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-checkpolling).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): upsertsclerk_id,email,name,role(NOTprofile_image_url).lib/auth/authenticate.ts(885 lines):syncUserIfNotExists()fallback, retry logic.lib/email/emailService.tsfor 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