Skip to content

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