Skip to content

Application Flows

Overview

This system handles user onboarding for Studio Jadu. There are two paths to account creation:

  1. Self-serve — user finds the page themselves, fills out the form, verifies email, gets account.
  2. Invited — admin sends an invite, user arrives with a token, fills out the form, gets account immediately (no email verification).

Both paths end at the same place: a jadu-auth account exists and the user is redirected to their studio profile.


Architecture

┌─────────────────────────────────────────────────────────────┐
│ Frontend (Next.js static export)                            │
│                                                             │
│  /                    → Onboarding flow (steps 1-5)         │
│  /verify?code=<token> → Email verification page             │
└────────────────────────────┬────────────────────────────────┘
                             │ HTTP
┌────────────────────────────▼────────────────────────────────┐
│ Worker (Cloudflare Worker + Hono)                           │
│                                                             │
│  POST /api/apply          → Self-serve submission           │
│  POST /api/apply/resend   → Resend verification email       │
│  POST /api/verify         → Email verification + account    │
│  POST /api/invite         → Admin sends invite (API-key)    │
│  GET  /api/invite/check   → Validate token + prefill data   │
│  POST /api/invite/submit  → Invited user submission         │
└────────────────────────────┬────────────────────────────────┘
                             │
┌────────────────────────────▼────────────────────────────────┐
│ External Services                                           │
│                                                             │
│  MongoDB        → formSubmissions, onboardingInvites,       │
│                   signUps collections                        │
│  jadu-auth      → POST /api/service/accounts (creates user) │
│  Resend         → Verification + confirmation emails        │
└─────────────────────────────────────────────────────────────┘

Flow 1: Self-Serve Application

User visits /
    │
    ▼
┌──────────────────────┐
│ Intro screen         │
│ "Start"              │
└──────────┬───────────┘
           ▼
┌──────────────────────┐
│ Step 1: Social links │
│ Step 2: Creator type │
│ Step 3: Effort level │  ← User fills out 5 steps
│ Step 4: Creator stage│
│ Step 5: What you'd   │
│         make         │
└──────────┬───────────┘
           ▼
┌──────────────────────┐
│ Review screen        │
│ "Looks good"         │
└──────────┬───────────┘
           ▼
┌──────────────────────┐
│ Name + Email screen  │
│ "Submit"             │
└──────────┬───────────┘
           │
           │  POST /api/apply
           │  Body: { name, email, responses }
           ▼
┌──────────────────────┐
│ Worker: ApplyService │
│ .submitPublicApplication() │
│                      │
│ 1. Check for existing│
│    application       │
│ 2. Generate token    │
│ 3. Save to           │
│    formSubmissions   │
│    (status: pending) │
│ 4. Send verification │
│    email via Resend  │
└──────────┬───────────┘
           ▼
┌──────────────────────┐
│ Confirmation screen  │
│ "Check your email"   │
└──────────────────────┘

           ... user checks email ...

┌──────────────────────┐
│ User clicks email    │
│ link → /verify?code= │
└──────────┬───────────┘
           │
           │  POST /api/verify
           │  Body: { token }
           ▼
┌──────────────────────┐
│ Worker: VerifyService│
│ .verifyApplicationAndCreateAccount() │
│                      │
│ 1. Find application  │
│    by token          │
│ 2. Check not expired │
│ 3. createJaduAuthAccount │
│    → jadu-auth       │
│ 4. Mark application  │
│    as verified       │
│ 5. Send confirmation │
│    email             │
│ 6. Return loginToken │
│    + landPath        │
└──────────┬───────────┘
           ▼
┌──────────────────────┐
│ /verify success page │
│ "You're all set"     │
│ Redirect → studio    │
│ profile with token   │
└──────────────────────┘

Flow 2: Invited User Application

Admin calls POST /api/invite
  Body: { email, name, links? }
  Header: x-api-key
    │
    ▼
┌──────────────────────────┐
│ Worker: InviteService    │
│ .sendSingleInvite()      │
│                          │
│ 1. Validate API key      │
│ 2. Check no active invite│
│ 3. Lookup signUps for    │
│    enrichment (links,    │
│    name, verified status)│
│ 4. Upsert to             │
│    onboardingInvites     │
│ 5. Send invite email     │
│    with token link       │
└──────────┬───────────────┘
           ▼
┌──────────────────────────┐
│ User receives email      │
│ Clicks link → /?token=   │
└──────────┬───────────────┘
           │
           │  Page loads → InviteChecker component
           │  GET /api/invite/check?token=<uuid>
           ▼
┌──────────────────────────┐
│ Worker: InviteService    │
│ .checkInviteToken()      │
│                          │
│ 1. Find invite by token  │
│ 2. Check not expired     │
│ 3. Check not flagged     │
│ 4. Return prefill data:  │
│    - links (if verified  │
│      + has content)      │
│    - name                │
│    OR null (unverified/  │
│    no links)             │
└──────────┬───────────────┘
           │
           │  Frontend stores token in state
           │  Prefills links + name if available
           ▼
┌──────────────────────────┐
│ Intro screen             │
│ "Start"                  │
└──────────┬───────────────┘
           ▼
┌──────────────────────────┐
│ Step 1: Social links     │
│   (prefilled if data)    │
│ Step 2: Creator type     │
│ Step 3: Effort level     │
│ Step 4: Creator stage    │
│ Step 5: What you'd make  │
└──────────┬───────────────┘
           ▼
┌──────────────────────────┐
│ Review screen            │
│ "Submit" (not "Looks     │
│  good" — token present)  │
│                          │
│ No name/email screen.    │
│ No email verification.   │
└──────────┬───────────────┘
           │
           │  POST /api/invite/submit
           │  Body: { token, responses }
           ▼
┌──────────────────────────┐
│ Worker: InviteService    │
│ .submitInvitedApplication() │
│                          │
│ 1. Find invite by token  │
│ 2. Validate: not expired,│
│    not already used,     │
│    not flagged, has      │
│    email + name          │
│ 3. createJaduAuthAccount │
│    → jadu-auth           │
│ 4. Save ApplyDocument    │
│    (status: verified)    │
│ 5. Mark invite as applied│
│ 6. Send confirmation     │
│    email                 │
│ 7. Return loginToken     │
└──────────┬───────────────┘
           ▼
┌──────────────────────────┐
│ Frontend redirects       │
│ → studio profile URL     │
│   with loginToken        │
└──────────────────────────┘

Flow 3: Bulk Invite

Admin calls POST /api/invite/bulk
  Body: { invites: [{ email, name }, ...] }  (max 70)
  Header: x-api-key
    │
    ▼
┌──────────────────────────────┐
│ Worker: InviteService        │
│ .sendBulkInvites()           │
│                              │
│ 1. Validate API key          │
│    (requireInviteAdminApiKey)│
│ 2. Deduplicate by email      │
│ 3. For each email:           │
│    - If active token exists  │
│      → skip                  │
│    - Otherwise → process     │
│ 4. Generate tokens for all   │
│    processable invites       │
│ 5. Bulk upsert to            │
│    onboardingInvites         │
│    (emailStatus: pending)    │
│ 6. Send batch email via      │
│    Resend (permissive mode)  │
│ 7. Update emailStatus per    │
│    result (sent / failed)    │
│ 8. Return summary            │
└──────────┬───────────────────┘
           ▼
┌──────────────────────────────┐
│ Response:                    │
│ {                            │
│   sent: 8,                   │
│   failed: 1,                 │
│   skipped: 1,                │
│   failures: [                │
│     { email, reason }        │
│   ]                          │
│ }                            │
└──────────────────────────────┘

Each invited user then follows the same
Flow 2 path (click link → fill form → submit).

Bulk Invite Behavior

Scenario Result
Email has no invite Processed — new invite created, email sent
Email has expired token Processed — invite upserted with fresh token
Email has active (non-expired) token Skipped — no duplicate email
Resend rejects email (bad domain, etc.) Failed — emailStatus: failed stored
Duplicate emails in same request Deduplicated — last occurrence wins

Limits

  • Max invites per request: 70 (Resend batch limit is 100, we leave headroom)
  • Resend batch mode: permissive — valid emails send, invalid ones return errors with index
  • Bounce tracking: Not yet implemented (requires Resend webhooks)

Collections

formSubmissions

Stores all applications regardless of how they arrived. One record per applicant.

Field Type Description
email string Applicant email (lowercase)
name string Applicant display name
responses object { links, creatorType, effortLevel, creatorStage, whatYoudMake }
verificationToken string UUID for email verification (self-serve)
tokenExpiresAt Date When the verification token expires
status enum pending_verification or verified
lastResendAt Date? Last time verification email was resent
verifiedAt Date? When the application was verified
landPath string? Deep-link path from ?lp= at apply time, returned by /api/verify. null for invited users and pre-existing docs.
createdAt Date Record creation time
updatedAt Date Last modification time

onboardingInvites

Stores invite records. One per invited email. Upserted on re-invite.

Field Type Description
email string Invited user's email (lowercase)
name string? Name from signUps or request body
links string[] Social/portfolio links (from signUps or request)
signupRef ObjectId? Reference to signUps doc (null for cold invites)
verified boolean Whether the user was verified in signUps
token string Invite token UUID
tokenExpiresAt Date 48 hours from invite creation
flagged boolean Whether the user is flagged (blocks submission)
invitedAt Date When the invite was sent
appliedAt Date? When the user submitted (null until used)
emailStatus enum pending, sent, or failed (tracks email delivery)

signUps (read-only)

Pre-existing collection from the landing page. The invite system reads from it to enrich invite data but never writes to it.


API Routes

Method Path Auth Description
POST /api/apply None Self-serve application submission
POST /api/apply/resend None Resend verification email
POST /api/verify None Verify email + create account
POST /api/invite x-api-key Admin sends a single invite
POST /api/invite/bulk x-api-key Admin sends up to 70 invites at once
GET /api/invite/check None Validate invite token + return prefill
POST /api/invite/submit None Invited user submits + gets account

Key Differences Between Flows

Aspect Self-Serve Invited
Entry point User visits / directly User clicks invite email → /?token=<uuid>
Name/email input User enters manually Pulled from invite record
Email verification Required (click link in email) Skipped entirely
Account creation timing After email click (POST /api/verify) Immediately on submit (POST /api/invite/submit)
Review screen button "Looks good" → goes to name/email "Submit" → submits directly
Post-submit UX Confirmation → "check your email" Direct redirect to studio profile
Token expiry 24 hours (verification token) 48 hours (invite token)

Error Handling

All errors follow the same response shape:

{
  "isSuccess": false,
  "error": "error_code",
  "message": "Human-readable message",
  "data": {}
}

Common error codes:

Code Status Meaning
validation_error 400 Zod schema validation failed
invalid_token 400 Token not found in DB
expired_token 400 Token exists but expired
already_applied 400 Invite already used
already_verified 409 jadu-auth account already exists
already_invited 409 Active invite already exists for this email
forbidden 403 Invalid API key
unauthorized 401 Missing or invalid API key (middleware)
rate_limited 429 Resend email too frequently
auth_config_error 500 jadu-auth API key rejected
account_creation_failed 500 jadu-auth returned unexpected error