Skip to content

API Router Dispatch (Code Level)

C4 Level 4 detail of the PSI API routing system. Shows how Hono processes requests, the module registry, admin authorization, and the derived views trigger system.

Hono Application Setup

The PSI backend uses Hono as its HTTP framework, shared between Firebase Cloud Functions and standalone deployments:

graph TD
    subgraph "Entry Points"
        FuncEntry["server/functions.ts<br/>Firebase Cloud Functions<br/>Exports 'api' via getRequestListener"]
        StdEntry["server/index.ts<br/>Standalone mode<br/>@hono/node-server on PORT 5001"]
    end

    subgraph "Shared Application"
        CreateApp["createApp()<br/>server/util/api.ts"]
        HonoApp["Hono App"]
    end

    FuncEntry --> CreateApp
    StdEntry --> CreateApp
    CreateApp --> HonoApp

Middleware Stack

sequenceDiagram
    participant Client as HTTP Client
    participant Hono as Hono App
    participant CORS as CORS Middleware
    participant Context as Context Injection
    participant Route as Route Handler
    participant Module as Module Function

    Client->>Hono: POST /api/moderation/checkCommentWithGpt

    Hono->>CORS: Process CORS headers
    Note over CORS: Permissive: reflects Origin header<br/>Allows all methods<br/>Exposes Authorization header
    CORS-->>Hono: CORS headers set

    Hono->>Context: Inject dependencies
    Note over Context: Sets c.set("authAdapter", ...)<br/>Sets c.set("databaseAdapter", ...)<br/>Sets c.set("modules", ...)
    Context-->>Hono: Context populated

    Hono->>Route: Match route pattern
    Note over Route: Matches /:componentId/:apiId<br/>OR /api/:componentId/:apiId

    Route->>Route: Extract componentId="moderation"<br/>Extract apiId="checkCommentWithGpt"
    Route->>Module: Resolve and dispatch

Module Resolution and Dispatch

sequenceDiagram
    participant Router as Route Handler
    participant Registry as Module Registry
    participant Auth as Auth Check
    participant Store as ServerStore Factory
    participant Module as Module Function

    Router->>Registry: getModule(componentId)
    Note over Registry: Module registry built from:<br/>1. Core modules (24)<br/>2. ZDF open modules (7)<br/>3. CBC/RC open modules (1)

    alt Module not found
        Registry-->>Router: null
        Router-->>Router: Return 404
    end

    Registry-->>Router: Module {publicFunctions, adminFunctions}

    Router->>Router: Look up apiId in module.publicFunctions

    alt apiId found in publicFunctions
        Router->>Auth: getSession(request)
        Auth-->>Router: Session {user, isValid}
        Router->>Store: Create ServerStore(silo, structure, instance, user)
        Router->>Module: module.publicFunctions[apiId](store, params)
    else apiId found in adminFunctions
        Router->>Auth: getSession(request)
        Auth-->>Router: Session {user}
        Router->>Store: Create ServerStore(...)
        Router->>Auth: store.getIsUserAdminAsync()
        alt User is admin
            Auth-->>Router: true
            Router->>Module: module.adminFunctions[apiId](store, params)
        else Not admin
            Auth-->>Router: false
            Router-->>Router: Return 403 Forbidden
        end
    else apiId not found
        Router-->>Router: Return 404
    end

    Module-->>Router: Response data
    Router->>Store: commitWrites()
    Router-->>Router: Return JSON/HTML/text/redirect

Module Registry Structure

classDiagram
    class Module {
        +key: string
        +publicFunctions: Record~string, Function~
        +adminFunctions: Record~string, Function~
    }

    class ModuleRegistry {
        -modules: Map~string, Module~
        +getModule(key) Module
        +getAllModules() Module[]
    }

    class CoreModules {
        admin
        analytics
        article
        auth
        constructor
        contentTranslation
        database
        debug
        derivedviews
        devlogin
        global
        health
        jigsaw
        language
        moderation
        notifs
        premodReview
        profile
        puppet
        questions
        ranking
        suspension
        topics
        users
    }

    class ZDFModules {
        articleTeaserPosition
        blocklist
        channel
        conversationhelper
        moderationZdf
        pushNotifications
        videovoting
    }

    class CBCRCModules {
        topicmigration
    }

    ModuleRegistry --> Module : contains
    CoreModules --> ModuleRegistry : registers 24 modules
    ZDFModules --> ModuleRegistry : registers 7 modules
    CBCRCModules --> ModuleRegistry : registers 1 module

Response Types

The API router supports multiple response formats:

graph TD
    A[Module function returns] --> B{Return type?}
    B -->|Object/Array| C["JSON Response<br/>Content-Type: application/json"]
    B -->|String starting with &lt;| D["HTML Response<br/>Content-Type: text/html"]
    B -->|Plain string| E["Text Response<br/>Content-Type: text/plain"]
    B -->|{redirect: url}| F["HTTP 302 Redirect"]
    B -->|null/undefined| G["204 No Content"]

Derived Views Trigger System

After data mutations, the derivedviews module fires triggers to update computed data:

graph TD
    subgraph "Object Triggers (fire on specific data changes)"
        T1["CommentsOnProfile<br/><em>Update profile comment count</em>"]
        T2["AnswerOnProfile<br/><em>Update profile answer count</em>"]
        T3["QuestionOnArticle<br/><em>Link questions to articles</em>"]
        T4["QuestionOnQuestion<br/><em>Related questions</em>"]
        T5["VideoVotesOnQuestion (ZDF)<br/><em>Aggregate video vote counts</em>"]
        T6["CommentsOnQuestion (ZDF)<br/><em>Update question comment counts</em>"]
        T7["QuestionOnTopic (CBC/RC)<br/><em>Link questions to topics</em>"]
        T8["TopicOnArticle (CBC/RC)<br/><em>Link topics to articles</em>"]
    end

    subgraph "Global Triggers (fire on instance-level changes)"
        T9["FeaturesOnQuestion (ZDF)<br/><em>Sync feature flags</em>"]
        T10["PreviewOnTopic (CBC/RC)<br/><em>Generate topic previews</em>"]
    end

    subgraph "Trigger Flow"
        DataWrite["Data Write<br/>(setObject, addObject)"] --> Check["derivedviews module<br/>checks registered triggers"]
        Check --> Match["Matching triggers fire"]
        Match --> Compute["Compute derived data"]
        Compute --> Store["Write derived data<br/>to module-public"]
    end

Health Check

graph LR
    A["GET /health/live"] --> B[health module]
    B --> C["Return 200 OK<br/>{status: 'ok', timestamp: ...}"]

The health endpoint is used by Docker health checks (wget --spider http://localhost:5001/health/live) and load balancer probes.

Further Reading