Data Model
PSI uses a structured data model designed for instance isolation, database portability, and easy security. All data access goes through the Datastore (client) and ServerStore (server) abstractions.
Data Types
PSI has four types of data:
graph TD
subgraph "Per-Instance Data"
P[Properties] -->|"e.g. question name"| I[Instance]
C[Collections] -->|"e.g. comments"| I
end
subgraph "Per-Module Data"
MD[Module Data] -->|"e.g. used pseudonyms"| M[Module]
end
subgraph "Client-Only"
SD[Session Data] -->|"e.g. sort mode, expanded state"| S[Browser Session]
end | Type | Scope | Read | Write | Example |
|---|---|---|---|---|
| Properties | Per-instance | All users | Admin only | Question name, topic description |
| Collections | Per-instance | All users | Creator of each object | Comments, reactions, votes |
| Module Data | Per-module | Varies by mode | Varies by mode | Used pseudonyms, language settings |
| Session Data | Per-session (client only) | Current user | Current user | Sort mode, expanded comments |
Module Data Access Modes
| Mode | Client Read | Client Write | Server Read | Server Write |
|---|---|---|---|---|
PRIVATE | No | No | Yes | Yes |
PUBLIC | All users | No | Yes | Yes |
USER_GLOBAL | Current user | Current user | Yes | Yes |
USER_READ_GLOBAL | Current user | No | Yes | Yes |
Module Data Structures
| Type | Description |
|---|---|
ModuleGlobal<T> | A single value |
ModuleMap<T> | Map from string to T |
ModuleMapMap<T> | Two-level map (key1 -> key2 -> T) |
Database Schema (Firebase RTDB)
silo/{siloKey}/
├── module/{moduleKey}/ # PRIVATE module data
├── module-public/{moduleKey}/ # PUBLIC module data
├── module-user/{userId}/{moduleKey}/ # USER_GLOBAL data
├── module-user-read/{userId}/{moduleKey}/ # USER_READ data
└── structure/{structureKey}/
└── instance/{instanceKey}/
├── collection/{typeName}/
│ └── {objectKey}/ # { from, time, ...fields }
└── global/
├── {propertyName} # Instance properties
└── features/ # Enabled features dict
Collection Object Fields
Every object in a collection always has:
| Field | Type | Description |
|---|---|---|
key | string | Unique identifier within the collection |
from | string | User ID of the creator (only they can edit it) |
time | number | Creation timestamp |
Client-Side Data Access
The Datastore class enforces instance isolation -- code can only access data on the instance the user is currently viewing:
// Declare data structures (once)
const nameProperty = declareInstanceProperty<string>('name');
const commentTable = declareTable<Comment>('comment');
// Read in component renders (reactive)
const name = useInstanceProperty(nameProperty);
const comments = useCollection(commentTable, { sortBy: 'time' });
// Write in callbacks
await datastore.addObject(commentTable, { text: 'My comment' });
await datastore.setInstancePropertyAsync(nameProperty, 'New Name'); // admin only
Server-Side Data Access
The ServerStore class is scoped per request. It can access the current instance easily, and other instances via explicit Remote methods:
async function myApiFunction(serverstore: ServerStore, params: MyParams) {
// Read from current instance
const name = await serverstore.getInstancePropertyAsync('name');
// Write to current instance (batched until function returns)
serverstore.setInstanceProperty('name', 'Updated Name');
// Access other instances via Remote methods
const otherName = await serverstore.getRemoteGlobalAsync({
instanceKey: 'other-instance',
key: 'name'
});
}
Batched Writes
Server writes are batched and committed atomically when the API function completes. This prevents inconsistent database state if a function fails mid-execution, but means reads within the same function won't see your own writes.
MongoDB Schema
When using the MongoDB adapter, the same data model maps to four collections:
| Collection | Document ID Pattern |
|---|---|
instances | {siloKey}:{structureKey}:{instanceKey} |
collection_objects | {siloKey}:{structureKey}:{instanceKey}:{collectionKey}:{objectKey} |
moduledata_{mode} | {siloKey}:{mode}:{moduleKey}:{userId}:{name}:{key1}:{key2} |
silos | {siloKey} |
Compound-prefixed keys ensure good data locality -- all data for one instance is clustered together.
Further Reading
- Data Model reference (psi-product) -- full API documentation
- Isolation Model -- how data isolation is enforced
- MongoDB Adapter (psi-product) -- MongoDB-specific details