Skip to content

Identity Module

Location: apps/api/src/modules/identity/

Aggregate roots: User, InviteCode, Persona


Invite Codes

Format

REG-XXXXXX
  • Prefix: REG-
  • 6 characters from alphabet A-Z2-9 (no 0/1 to avoid visual confusion)
  • Generated with crypto.randomInt — cryptographically secure, not Math.random
  • Example: REG-T4K9XA

Allocation Formula

The number of codes a user can generate is capped by their reputation and account age:

typescript
const allotment = clamp(5 + Math.floor(rep / 50) + Math.floor(ageDays / 14), 5, 30);
FactorContribution
Base5 codes
Reputation+1 per 50 total rep points
Account age+1 per 14 days since registration
Minimum5
Maximum30

In Alpha, codes have a 30-day validity window from generation. Expired unused codes do not count towards the cap.

Reserve → Register Flow

mermaid
sequenceDiagram
    participant User
    participant API

    User->>API: POST /auth/reserve-invite {code}
    Note over API: Check code is 'active'
    API-->>User: {sessionId, expiresAt (+30 min)}

    Note over User,API: User fills registration form

    User->>API: POST /auth/register {sessionId, email, password, ...}
    Note over API: Validate sessionId not expired<br/>Create user, mark code 'used'<br/>Create InviteNode<br/>Bootstrap EmojiWallet (50 mana)
    API-->>User: {accessToken, user}

TTL: Reserved codes revert to active if the registration is not completed within 30 minutes. The cleanup runs via database query on each reserve attempt.

Code Status Lifecycle

active → reserved → used
active → expired (TTL sweep)
active → revoked (admin action)

Personas

Users can create up to 5 personas. One persona is always marked isDefault = true — this is created automatically as Main on registration.

Persona Fields

FieldTypeNotes
namevarchar(40)Unique per user
aboutvarchar(280)Short bio
accentvarchar(20)Color token: ink | gold | sage | azure
profilePicUrltext?Separate avatar per persona
isDefaultboolOnly one true per user

Usage

Posts can be attributed to a persona via Post.personaId. A null personaId means the post is under the user's default persona. The composer shows a picker for switching between personas.

API

MethodPathDescription
GET/me/personasList all personas
POST/me/personasCreate (max 5 enforced)
PATCH/me/personas/:idUpdate name, about, accent
DELETE/me/personas/:idDelete (cannot delete default if others exist)
POST/me/personas/:id/set-defaultPromote to default

Registration Requirements

FieldRequiredValidation
emailyesValid email, unique
passwordyes≥ 8 chars
firstNameyes
lastNameyes
dateOfBirthyesMust be ≥ 18
countryyesISO 3166-1 alpha-2
cityyes
sessionIdyesValid non-expired reservation

Password Reset

  1. POST /auth/forgot-password {email} → generates short opaque token, sends email (Resend). In Alpha without RESEND_API_KEY, the token is logged to stdout.
  2. Token is valid for 1 hour.
  3. POST /auth/reset-password {token, newPassword} → updates passwordHash, marks token usedAt.

GDPR Deletion

DELETE /me triggers a cascade delete from users table, propagating to all related rows via Prisma onDelete: Cascade. The user's invite codes are marked revoked (not deleted — for audit). AuditLogEntry rows referencing the user are kept with the targetId preserved as a string (no real FK).


Events Emitted

EventPayloadTrigger
identity.user_registered{userId, email, inviterUserId?}After user row created
identity.invite_code.used{codeId, newUserId, inviterUserId}After registration

Regulus — invite-only social-knowledge platform