Application Flows¶
Overview¶
This system handles user onboarding for Studio Jadu. There are two paths to account creation:
- Self-serve — user finds the page themselves, fills out the form, verifies email, gets account.
- 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 |