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
- Data Model -- data access patterns and hooks
- Adapter Pattern -- all adapter interfaces
- Isolation Model -- how instance and module isolation is enforced
- MongoDB Adapter (psi-product) -- MongoDB specifics