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 <| 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
- Component View -- C4 Level 3 backend components
- Adapter Pattern -- how adapters are injected into the router context
- Isolation Model -- how ServerStore enforces data isolation