Skip to content

Auth & Roles

JWT

All API endpoints are protected with JWT Bearer authentication (except POST /auth/register, POST /auth/login, GET /health).

JWT Payload

typescript
interface JwtPayload {
  sub: string;    // User UUID (internal, never exposed publicly)
  email: string;  // User email
  role: string;   // 'user' | 'moderator' | 'admin'
  iat: number;    // Issued at (unix seconds)
  exp: number;    // Expiry (unix seconds)
}

Invariant: The JWT payload is stable. Do not extend it without an ADR. Persona, interests, and other context-specific data are fetched separately.

Token Lifetime

Default expiry: 7 days. No refresh token in Alpha — re-login required after expiry.


Role Hierarchy

RoleWhoCapabilities
userAll registered usersStandard platform actions
moderatorAssigned by adminHide/unhide posts and replies, review reports
adminManually assignedAll moderator powers + role change, ban, feature flags, audit log, broadcast

Role is stored in users.role and embedded in the JWT. Privilege escalation requires an admin to update users.role directly.


Guards

JwtAuthGuard

Applied globally via APP_GUARD. Every request must present a valid Authorization: Bearer <token> header unless the route is explicitly marked @Public().

typescript
// Skip auth for public endpoints
@Public()
@Post('/auth/register')
async register(@Body() dto: RegisterDto) { ... }

AdminGuard

Applied on top of JwtAuthGuard for admin-only endpoints. Checks user.role === 'admin'.

typescript
@UseGuards(AdminGuard)
@Get('/admin/users')
async listUsers() { ... }

ModeratorGuard

Checks user.role === 'moderator' || user.role === 'admin'.

ThrottlerGuard

Applied globally. Default: 60 requests per minute per IP. Per-route overrides for sensitive endpoints (e.g. POST /auth/login is 5/min).


Access Matrix

Reputation-based capability checks live in AccessMatrixService (apps/api/src/shared/access/).

typescript
export type Capabilities = {
  canVoteWiki: boolean;       // total rep ≥ 5
  canMentor: boolean;         // rep ≥ 30 in interest, or total ≥ 50
  canModerate: boolean;       // role = moderator | admin
  canProposeGuild: boolean;   // Wave 3 — always false in Alpha
  canPostJournalism: boolean; // rep ≥ 20 in ANY single interest
};

Client endpoint: GET /access/me returns the current user's full Capabilities object.

@RequiresRep Decorator

typescript
@RequiresRep({ interest: 'philosophy', min: 10 })
@Post('/wiki/proposals')
async createProposal(@Body() dto: CreateProposalDto) { ... }

Injects AccessMatrixService check; throws ForbiddenException if not met.


Auth Flow

mermaid
sequenceDiagram
    participant Client
    participant API
    participant DB

    Client->>API: POST /auth/reserve-invite {code}
    API->>DB: inviteCode.status = 'reserved', reservedUntil = now+30min
    API-->>Client: {sessionId, expiresAt}

    Client->>API: POST /auth/register {sessionId, email, password, ...}
    API->>DB: create User, mark inviteCode 'used'
    API->>DB: create InviteNode, bootstrap EmojiWallet (50 mana)
    API-->>Client: {accessToken, user}

    Client->>API: POST /auth/login {email, password}
    API-->>Client: {accessToken, user}

    Note over Client,API: All subsequent requests include Authorization: Bearer <token>

Public Actor Identifiers

Internal UUID (users.id) is never exposed directly in public-facing APIs. Public identifiers use Webfinger-style format:

This is the federation-ready format that will map to ActivityPub actors in v2.0.

Regulus — invite-only social-knowledge platform