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