Skip to content

Database Adapter (Code Level)

C4 Level 4 detail of the PSI data storage layer. Shows the database adapter interface, ServerStore scoping, client-side adapters, and data schema.

Server-Side Database Adapter

classDiagram
    class DatabaseAdapter {
        <<interface>>
        +readObject(path) Promise~any~
        +writeObject(path, value) Promise~void~
        +readCollection(path) Promise~Record~
        +addObject(path, value) Promise~string~
        +deleteObject(path) Promise~void~
        +readModuleData(params) Promise~any~
        +writeModuleData(params, value) Promise~void~
        +getSiloConfig(siloKey) Promise~Config~
    }

    class FirebaseAdapter {
        -db: FirebaseDatabase
        +readObject(path)
        +writeObject(path, value)
        +readCollection(path)
        +addObject(path, value)
        +deleteObject(path)
        +readModuleData(params)
        +writeModuleData(params, value)
        +getSiloConfig(siloKey)
    }

    class MongoDBAdapter {
        -client: MongoClient
        -db: Db
        +readObject(path)
        +writeObject(path, value)
        +readCollection(path)
        +addObject(path, value)
        +deleteObject(path)
        +readModuleData(params)
        +writeModuleData(params, value)
        +getSiloConfig(siloKey)
    }

    DatabaseAdapter <|.. FirebaseAdapter : DATABASE_ADAPTER=firebase (default)
    DatabaseAdapter <|.. MongoDBAdapter : DATABASE_ADAPTER=mongodb

    note for FirebaseAdapter "Uses Firebase Admin SDK.\nData at: silo/{siloKey}/structure/..."
    note for MongoDBAdapter "Uses compound-prefixed keys.\n4 collections: instances,\ncollection_objects, moduledata_*, silos"

ServerStore Architecture

The ServerStore is the core server-side data access layer. It is scoped per API request and enforces isolation.

classDiagram
    class ServerStore {
        -siloKey: string
        -structureKey: string
        -instanceKey: string
        -userId: string
        -databaseAdapter: DatabaseAdapter
        -pendingWrites: Write[]
        +getObject(typeName, key) Promise~any~
        +setObject(typeName, key, value) void
        +addObject(typeName, value) string
        +getCollection(typeName) Promise~Record~
        +getModuleData(mode, params) Promise~any~
        +setModuleData(mode, params, value) void
        +commitWrites() Promise~void~
        +getIsUserAdminAsync() Promise~boolean~
    }

    class Write {
        +path: string
        +value: any
        +operation: "set" | "add" | "delete"
    }

    class DataAccessMode {
        <<enumeration>>
        PRIVATE
        PUBLIC
        USER_GLOBAL
        USER_READ_GLOBAL
    }

    ServerStore "1" --> "*" Write : batches
    ServerStore --> DataAccessMode : uses modes
    ServerStore --> DatabaseAdapter : delegates to

Request-Scoped Store Creation

sequenceDiagram
    participant Router as Hono API Router
    participant Store as ServerStore Factory
    participant Module as Module Function
    participant DB as Database Adapter

    Router->>Router: Parse /:moduleId/:apiId
    Router->>Router: Extract silo, structure, instance from request context
    Router->>Store: new ServerStore(silo, structure, instance, user, dbAdapter)
    Note over Store: Store is scoped to this<br/>specific request context

    Router->>Module: module.publicFunctions[apiId](store, params)

    Module->>Store: getCollection("comment")
    Store->>DB: readCollection("silo/zdf/structure/question/instance/abc123/collection/comment")
    DB-->>Store: {key1: {...}, key2: {...}}
    Store-->>Module: Comment collection data

    Module->>Store: setObject("comment", "new1", {text: "Hello", from: userId})
    Note over Store: Write is BATCHED, not executed yet

    Module->>Store: setModuleData(PUBLIC, {module: "moderation", name: "status"}, "approved")
    Note over Store: Another batched write

    Module-->>Router: Function returns

    Router->>Store: commitWrites()
    Store->>DB: Execute all batched writes atomically
    DB-->>Store: Success

Data Access Modes

Module data can be stored with different access levels:

graph TD
    subgraph "Per-Instance Module Data"
        PRIVATE["PRIVATE<br/>silo/{s}/module/{m}/<br/>Server read/write only"]
        PUBLIC["PUBLIC<br/>silo/{s}/module-public/{m}/<br/>All users can read,<br/>server writes only"]
    end

    subgraph "Per-User Module Data"
        USER_GLOBAL["USER_GLOBAL<br/>silo/{s}/module-user/{userId}/{m}/<br/>User reads and writes own data"]
        USER_READ["USER_READ_GLOBAL<br/>silo/{s}/module-user-read/{userId}/{m}/<br/>User reads own, server writes"]
    end

    ServerStore --> PRIVATE
    ServerStore --> PUBLIC
    ServerStore --> USER_GLOBAL
    ServerStore --> USER_READ

Firebase RTDB Data Schema

graph TD
    Root["/"] --> Silo["silo/{siloKey}"]

    Silo --> Module["module/{moduleKey}<br/><em>PRIVATE data</em>"]
    Silo --> ModulePub["module-public/{moduleKey}<br/><em>PUBLIC data</em>"]
    Silo --> ModuleUser["module-user/{userId}/{moduleKey}<br/><em>USER_GLOBAL data</em>"]
    Silo --> ModuleUserRead["module-user-read/{userId}/{moduleKey}<br/><em>USER_READ_GLOBAL data</em>"]

    Silo --> Structure["structure/{structureKey}"]
    Structure --> Instance["instance/{instanceKey}"]

    Instance --> Collection["collection/{typeName}/{objectKey}<br/><em>Comments, answers, etc.</em>"]
    Instance --> Global["global/<br/><em>Server-only instance props</em>"]
    Instance --> Features["global/features/<br/><em>Enabled features dictionary</em>"]

    Root --> Log["log/<br/><em>Event logs (indexed by sessionKey, eventType, time)</em>"]

MongoDB Schema Mapping

graph LR
    subgraph "MongoDB Collections"
        Instances["instances<br/>key: {silo}:{structure}:{instance}"]
        Objects["collection_objects<br/>key: {silo}:{structure}:{instance}:{collection}:{objectKey}"]
        ModPrivate["moduledata_private<br/>key: {silo}:private:{module}:{userId}:{name}:{key1}:{key2}"]
        ModPublic["moduledata_public<br/>key: {silo}:public:{module}:..."]
        ModUser["moduledata_user<br/>key: {silo}:user:{module}:{userId}:..."]
        Silos["silos<br/>key: {siloKey}"]
    end

    subgraph "Firebase RTDB Equivalent"
        F1["silo/{s}/structure/{str}/instance/{i}/"]
        F2["silo/{s}/structure/{str}/instance/{i}/collection/{type}/{key}"]
        F3["silo/{s}/module/{m}/"]
        F4["silo/{s}/module-public/{m}/"]
        F5["silo/{s}/module-user/{u}/{m}/"]
    end

    Instances -.-> F1
    Objects -.-> F2
    ModPrivate -.-> F3
    ModPublic -.-> F4
    ModUser -.-> F5

Client-Side Database Adapters

classDiagram
    class ClientDatabaseAdapter {
        <<interface>>
        +watchObject(path, callback) Unsubscribe
        +watchCollection(path, callback) Unsubscribe
        +writeObject(path, value) Promise~void~
        +addObject(path, value) Promise~string~
    }

    class FirebaseDatabaseAdapter {
        -db: FirebaseDatabase
        +watchObject(path, callback)
        +watchCollection(path, callback)
        +writeObject(path, value)
        +addObject(path, value)
    }

    class ServerDatabaseAdapter {
        -apiBaseUrl: string
        +watchObject(path, callback)
        +watchCollection(path, callback)
        +writeObject(path, value)
        +addObject(path, value)
    }

    class DemoDatabaseAdapter {
        -data: Map
        +watchObject(path, callback)
        +watchCollection(path, callback)
        +writeObject(path, value)
        +addObject(path, value)
    }

    ClientDatabaseAdapter <|.. FirebaseDatabaseAdapter : direct WebSocket
    ClientDatabaseAdapter <|.. ServerDatabaseAdapter : all via REST API
    ClientDatabaseAdapter <|.. DemoDatabaseAdapter : in-memory

    note for FirebaseDatabaseAdapter "Default. Direct Firebase SDK connection.\nReal-time updates via WebSocket.\nUsed by most silos."

    note for ServerDatabaseAdapter "All reads/writes proxied through PSI backend.\nUsed by SRG silo and puppet-via-server.\nNo direct client-to-database access."

    note for DemoDatabaseAdapter "In-memory storage for tests and demos.\nNo external dependencies."

Adapter Selection

graph TD
    A[psi.config.ts] --> B{silo.databaseAdapter?}
    B -->|"'server'"| C[ServerDatabaseAdapter<br/>SRG, puppet-via-server silos]
    B -->|undefined / default| D[FirebaseDatabaseAdapter<br/>Most production silos]
    B -->|"'demo'"| E[DemoDatabaseAdapter<br/>Component demos, tests]

Further Reading