Users and Groups
Who is allowed to do what, and why we don't care about their phone numbers.
Users and Groups are the foundational identities within Platrium. Unlike rigid relational databases, Platrium uses a Graph Database (Neo4j/Memgraph) to model these identities, allowing for flexible, heavily-connected authorization structures and rapid edge traversals.
The User Model
Every user in Platrium is represented by a User node, and they inherently belong to a Tenant.
{
"id": "usr_123xyz",
"idpId": "idp_google_1",
"externalId": "104293847291",
"email": "admin@acme.com",
"displayName": "Admin",
"createdAt": 1727670000
}Minimal Profile Data Design
You might notice that the User node is extremely lightweight. We deliberately only store email and displayName, ignoring extended profile data like phone numbers, physical addresses, or job titles.
The Group Model
Groups are structural collections that users can be assigned to for bulk authorization (e.g., granting folder access to an entire team).
{
"id": "grp_marketing",
"tenantId": "tnt_acme",
"name": "Marketing Team",
"createdAt": 1727670000
}In the Graph Database, Groups act as authorization hubs. A Tenant owns the Group, Users are members of the Group, and the Group is granted access to Files/Folders.
IdP Linking (Single Source of Truth)
Notice that the User node explicitly contains an idpId property. This property maps the user back to the specific Identity Provider (like Google Workspace, Okta, or Local Auth) that controls their authentication.
The User Orchestrator
Because Platrium spans a Graph Database, a KV Store (AuthN Secrets), and a distributed storage engine (Files), users cannot just be written to the database with a raw SQL/Cypher insert.
All user provisioning and deprovisioning is heavily guarded by the UserOrchestrator (located in core/internal/orchestrators/user.go).
Provisioning Flow
When a new user is created (either via Admin invite or JIT SCIM provisioning), the UserOrchestrator coordinates across multiple domains within a single, monolithic database transaction.
By explicitly passing the tx graph.Tx pointer down into the Storage engine (FSOps), we guarantee that the User's identity and their underlying storage architecture are created atomically. If the drive fails to provision, the user account creation completely rolls back.
Deprovisioning & Background Deletes
If a Tenant Admin deletes an IdP connection (e.g., removing a legacy Okta integration), we do not use a database-level cascading DETACH DELETE to wipe out the users.
Because a User is a distributed entity, a blind database delete would leave gigabytes of orphaned zombie data in the object storage and KV stores.
Instead, when an IdP is deleted:
- The system queries the B-Tree index for all associated users (
MATCH (u:User {idpId: $idpId})). - It pushes those user IDs into a background worker queue.
- The background worker calls the
UserOrchestrator.DeprovisionUser()workflow. - The Orchestrator systematically tears down their KV secrets, triggers the Storage Engine to aggressively garbage-collect their drives, and finally deletes the Graph node.