---
title: Registration Flow
tags: [flow, auth, signup, email-verification, referral]
related_models: ["[[User]]", "[[TempVerification]]"]
related_apis: ["POST /api/auth/register", "POST /api/auth/verify-email-code", "POST /api/auth/resend-verification"]
---

# Registration Flow

> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md))

End-to-end specification for **email + password** registration with role selection (buyer/seller), six-digit email verification, optional referral code attribution, and terms acceptance.

## Actors

- **Prospective User** – submits the sign-up form.
- **Frontend** – `frontend/src/auth/view/jwt/jwt-sign-up-view.tsx`, calling `signUp()` and later `verifyEmailWithCode()` from `frontend/src/auth/context/jwt/action.ts`.
- **Backend** – `AuthController.register` and `AuthController.verifyEmailWithCode` in `backend/src/services/auth/authController.ts`.
- **MongoDB** – `TempVerification` collection (temporary), then `User` collection (final).
- **Email service** – `backend/src/services/email/emailService.ts` (SMTP/transactional provider) — `sendVerificationCodeEmail()`.
- **Socket.IO** – emits `referral-signup` to the referrer if a referral code is supplied.

## Preconditions

- The email is not already a verified `User`. If a `TempVerification` already exists, its code and metadata are **regenerated and resent** rather than throwing a conflict.
- Outbound SMTP credentials are configured (`EMAIL_*` env vars consumed by `emailService.ts`).
- If a `referralCode` is supplied, it does **not** need to exist for sign-up to succeed — invalid codes are silently ignored at verification time.

## State machine: `TempVerification → User`

```mermaid
stateDiagram-v2
    [*] --> NotStarted
    NotStarted --> TempCreated: POST /api/auth/register\nemail + role [+ ref]
    TempCreated --> TempCreated: POST /api/auth/resend-verification\n(new code, 15-min TTL)
    TempCreated --> TempExpired: 15 minutes elapse\nor verification fails
    TempExpired --> TempCreated: User clicks "Resend"
    TempCreated --> UserActive: POST /api/auth/verify-email-code\n(code + password)
    UserActive --> [*]
    note right of TempCreated
        TempVerification document holds:
        email, firstName, lastName, role,
        referralCode, code, codeExpires
    end note
    note right of UserActive
        User created with isEmailVerified=true,
        status="active"; tokens issued immediately.
    end note
```

## Step-by-step narrative

### Phase 1 — Submit registration

1. **User visits `/auth/jwt/sign-up`** (optionally with `?ref=ABCD1234` from the short-URL referral redirect implemented at `backend/src/app.ts:274-278`).
2. **User selects role** (buyer or seller), enters email, password (held in client state only), accepts the Terms checkbox, and clicks "Create account".

> [!bug] ⚠️ KNOWN BUG / quirk — the sign-up form does not collect the real password
> `jwt-sign-up-view.tsx` `onSubmit` calls `signUp({ ..., password: '' })` with a **hard-coded empty string** (`jwt-sign-up-view.tsx:191`, with the inline comment `// You might need to add password field to form`). So the actual password is **not** collected on the sign-up form at all — it is collected at the **email-verification step** (`/verify-email-code`). The `TempVerification.password` field is effectively **unused** (it is set to `''` and never read as a real credential). The credential that ends up on the `User` is the one entered at verification.

3. **HTTP request**: `POST /api/auth/register` with `{ email, password: '', firstName?, lastName?, role, referralCode? }`. The frontend passes `password: ''` (empty string) — see the quirk above. The controller persists this empty string into `TempVerification.password`, which is never used as a real credential.
4. **Validation middleware** `registerValidation` (`authValidation.ts`) checks email format, password complexity, and role enum.
5. **Duplicate check** (`authController.ts:55-64`): `User.findOne({ email })` — if found, returns `409 USER_EXISTS`.
6. **Idempotent temp record**: `TempVerification.findOne({ email })` — if present, the existing temp is **updated in place** (new name, role, referralCode, fresh 6-digit code, expiry pushed to now + 15 min).
7. **Verification code**: `authService.generateVerificationCode()` (`authService.ts:226-228`) returns a uniformly random 6-digit string.
8. **Persistence**: A new `TempVerification` is saved with `{ email, password: '', firstName: defaults to "کاربر", lastName: defaults to "جدید", role, referralCode, emailVerificationCode, emailVerificationCodeExpires }`.
9. **Email dispatch**: `emailService.sendVerificationCodeEmail(email, firstName, code)` is called. The email contains the 6-digit code, branding, and a 15-minute expiry notice. Failure to send is logged but the response still succeeds with `201` (the user can resend).
10. **Response**: `{ email, message: "Verification code sent to email" }` with HTTP `201` for first-time, `200` for resend.
11. **Frontend** transitions to the OTP screen `/auth/jwt/verify?email=...` (`frontend/src/auth/view/jwt/jwt-verify-view.tsx`).

### Phase 2 — Verify code and finalise

12. **User enters the 6-digit code** and confirms the password. The password may be re-entered here for safety.
13. **HTTP request**: `POST /api/auth/verify-email-code` with `{ email, code, password }`.
14. **Format guard**: `authService.isValidVerificationCode(code)` enforces `/^\d{6}$/` (`authService.ts:236-238`).
15. **Lookup**: `TempVerification.findOne({ email, emailVerificationCode: code, emailVerificationCodeExpires: { $gt: now } })` — if any field mismatches or the code is older than 15 minutes, returns `400`.
16. **Hash password**: `bcrypt.hash(password, 12)` via `authService.hashPassword()`.
17. **Create `User`** (`authController.ts:400-435`): `email`, `password: hashedPassword`, `firstName`, `lastName`, `role`, `isEmailVerified: true`, `status: "active"`.
18. **Apply referral** (`authController.ts:691-713`): `tempVerification.referralCode` (stored on the `TempVerification` document at registration and applied here at verification) is looked up via `User.findOne({ referralCode })`. If a referrer is found:
    - `user.referredBy = referrer._id`
    - `referrer.referralStats.totalReferrals += 1`
    - Emit `referral-signup` on `user-${referrer._id}` Socket.IO room (`authController.ts:704`; the equivalent Google/other path emits at `authController.ts:1132`) — see [[Referral Flow]] for the points-awarding side effect that happens later on the first purchase.
    - ⚠️ **No self-referral guard**: the code only checks `if (referrer)` — it never compares `referrer._id` to the newly created user. A user who somehow signs up with their own `referralCode` would be attributed as their own referrer.
19. **Persist user**, then **delete** the TempVerification document (`findByIdAndDelete`).
20. **Token issuance**: identical to [[Authentication Flow]] — generate access + refresh, push the refresh into `user.refreshTokens[]`.
21. **Response**: `{ user, tokens: { accessToken, refreshToken } }`. Frontend writes both into `localStorage` (`action.ts:228-235`) and routes the user into the appropriate dashboard (`/dashboard/buyer` or `/dashboard/seller`).

## Sequence diagram

```mermaid
sequenceDiagram
    autonumber
    actor U as User
    participant FE as Frontend
    participant BE as Backend
    participant DB as MongoDB
    participant MAIL as Email Service
    participant IO as Socket.IO

    U->>FE: Fill sign-up form (email, role, ref?, password)
    FE->>BE: POST /api/auth/register
    BE->>DB: User.findOne({ email })
    DB-->>BE: null
    BE->>DB: TempVerification.findOne({ email })
    DB-->>BE: null
    BE->>BE: code = generateVerificationCode()
    BE->>DB: TempVerification.create({...code, expires=+15m})
    BE->>MAIL: sendVerificationCodeEmail(email, firstName, code)
    MAIL-->>U: Email with 6-digit code
    BE-->>FE: 201 { email, message }
    FE-->>U: Redirect /auth/jwt/verify

    U->>FE: Enter code + (re)password
    FE->>BE: POST /api/auth/verify-email-code { email, code, password }
    BE->>DB: TempVerification.findOne({ email, code, expires>now })
    DB-->>BE: tempVerification doc
    BE->>BE: hashPassword(password)
    BE->>DB: User.create({...isEmailVerified:true, status:active})
    opt referral present
        BE->>DB: User.findOne({ referralCode })
        DB-->>BE: referrer
        BE->>DB: referrer.referralStats.totalReferrals += 1
        BE->>IO: emit user-{refId} 'referral-signup'
    end
    BE->>DB: TempVerification.findByIdAndDelete(...)
    BE->>BE: generate tokens
    BE->>BE: push refresh
    BE-->>FE: 200 { user, tokens }
    FE->>FE: localStorage.setItem(accessToken, refreshToken)
    FE-->>U: Redirect /dashboard/{role}
```

## API calls

| Method | Endpoint | Source |
|---|---|---|
| `POST` | `/api/auth/register` | `authRoutes.ts:21` → `authController.register` |
| `POST` | `/api/auth/verify-email-code` | `authRoutes.ts:34` → `authController.verifyEmailWithCode` |
| `POST` | `/api/auth/resend-verification` | `authRoutes.ts:36-40` → `authController.resendVerificationEmail` |
| `GET` | `/r/:code` | `app.ts:274-278` — short-URL redirect that injects `?ref=` into the sign-up page |
| `POST` | `/api/auth/force-verify-user` | Dev-only — `authController.forceVerifyUser` (rejects outside `NODE_ENV=development`) |

## Database writes

- **`tempverifications` collection**: insert on first POST (carrying `email`, `password: ''`, `firstName`, `lastName`, `role`, `referralCode`, code + expiry), in-place update on duplicate POST, delete on successful verification.
- **`users` collection**: full insert on successful verification (`authController.ts:680-688`). The first refresh token is appended in the same save.
- **`users` collection (referrer)**: `referralStats.totalReferrals` incremented (`authController.ts:699`).

## Socket events emitted

- **`referral-signup`** → `user-${referrerId}` room when a referred user verifies. Payload:
  ```
  { userId, userName, userEmail, timestamp, totalReferrals }
  ```
  Source: `authController.ts:704-710` (and `:1132` on the parallel path).

## Side effects

- **Email**: one transactional message per `/register` and per `/resend-verification`. Content is generated by `emailService.sendVerificationCodeEmail`. Plain-text fallback included.
- **Sentry**: errors during `User.create` or email dispatch are captured server-side.
- **Logs**: the controller `console.log`s the generated code in **all environments** (`authController.ts:88`, `:117`, `:518`). Useful in dev; in prod the same log line ends up in CloudWatch/Sentry breadcrumbs. (Tracked as a hardening item.)

> [!warning] Verification code is logged server-side
> The generated 6-digit code is `console.log`-ed by the controller even in production. Anyone with log access can take over an unverified account. Move behind `if (NODE_ENV !== 'production')`.

## Error / edge cases

- **Email already registered (verified)** → `409 USER_EXISTS`.
- **Email already in temp (unverified)** → `200`, code regenerated, email re-sent. User-friendly; no error.
- **Code mismatch / expired (>15 min)** → `400 Invalid or expired verification code`. The TempVerification is **not** deleted, so the user can request a new code via "Resend".
- **Code format wrong (non-digits or wrong length)** → `400` from `isValidVerificationCode` guard before DB lookup.
- **Email delivery failure** → response still `201`/`200`; the user can hit "Resend" or check spam.
- **Referral code that does not match any user** → silently ignored; the user is still created with `referredBy: undefined`.
- **Self-referral** → **not guarded**. The referral attribution (`authController.ts:691-713`) only checks that a referrer exists, never that it differs from the signing-up user.
- **Race condition: two parallel registrations for the same email** → MongoDB unique index on `User.email` ensures only one user document; the loser of the race sees `E11000` and returns `409 USER_EXISTS`.
- **Race condition: verify request arrives twice with the same code** → second request finds no TempVerification and returns `400`. The created `User` is the canonical record.
- **Role tampering** → role is validated by `registerValidation` enum (`buyer | seller`). Admin role is created only via the bootstrap seed (`initializeAdminUser` in `app.ts:377`), never via this flow.

## Defaults & quirks

- `firstName` / `lastName` are not required by the frontend in many sign-up variants; the controller defaults them to Persian placeholders `"کاربر"` / `"جدید"` (`authController.ts:52-53`). They can be edited later under `/dashboard/account/profile`.
- The TempVerification TTL is enforced by the `emailVerificationCodeExpires` check, not by a Mongo TTL index — expired docs remain in the collection until overwritten or manually purged.

## Linked flows

- [[Authentication Flow]] — the next time the user signs in (includes the Telegram first-class auth section).
- [[Referral Flow]] — full points-awarding mechanics triggered here.
- [[Google OAuth Flow]] — alternative path that bypasses `TempVerification` (Google identities are pre-verified).
- [[Password Reset Flow]] — if the user forgets the password they set during verification.

> [!tip] Telegram — zero-step registration
> Users who open the Amanat Telegram Mini App do **not** go through this flow at all. `POST /api/auth/telegram` verifies the Telegram-signed `initData` and auto-provisions a new `User` (no email, `authProvider: "telegram"`) in a single round-trip. The `TempVerification` + email code cycle only applies to email-based sign-ups. See [[Authentication Flow#Telegram first-class auth flow]].

## Source files

- Backend: `backend/src/services/auth/authController.ts:33-158` (register), `:364-469` (verify), `:498-539` (resend)
- Backend: `backend/src/services/auth/authValidation.ts` (validation rules)
- Backend: `backend/src/models/TempVerification.ts` (temp schema)
- Backend: `backend/src/services/email/emailService.ts` (`sendVerificationCodeEmail`)
- Backend: `backend/src/app.ts:274-278` (short referral redirect)
- Frontend: `frontend/src/auth/view/jwt/jwt-sign-up-view.tsx`
- Frontend: `frontend/src/auth/view/jwt/jwt-verify-view.tsx`
- Frontend: `frontend/src/auth/context/jwt/action.ts:121-256`
