Domain model
The data model distinguishes logical files, immutable content versions, mutable current classification, immutable classification history, and access policy. For definitions of Root Folder, subfolder, file, group, metadata definition, tag, access grant, and audit event, see Glossary.
erDiagram
ORGANIZATION ||--o{ ORGANIZATION_MEMBERSHIP : has
USER ||--o{ ORGANIZATION_MEMBERSHIP : joins
USER ||--o{ GROUP_MEMBERSHIP : belongs_to
GROUP ||--o{ GROUP_MEMBERSHIP : contains
USER ||--o{ GROUP : owns
GROUP ||--o{ GROUP_INVITATION : offers
USER ||--o{ GROUP_INVITATION : invited
USER ||--o{ FOLDER : owns
FOLDER ||--o{ FILE : classifies
USER ||--o{ FILE : owns
FILE ||--|{ FILE_VERSION : contains
FILE ||--o{ FILE_METADATA_VALUE : has
METADATA_DEFINITION ||--o{ FILE_METADATA_VALUE : validates
FILE ||--o{ FILE_METADATA_REVISION : records
USER ||--o{ ACCESS_GRANT : grants
FILE ||--o{ ACCESS_GRANT : scopes
FOLDER ||--o{ ACCESS_GRANT : scopes
USER ||--o{ AUDIT_EVENT : acts_in
Logical modules
| Record | Purpose | Key invariant | |||
|---|---|---|---|---|---|
organizations | Explicit ownership boundary | One seeded organization in MVP | |||
users | Identity and account status | Deactivated users lose session access | |||
organization_memberships | Organization role | Role is global_admin, admin, or user | |||
groups / group_memberships | User-owned team grant subjects | Creator is sole owner; ≥1 active owner always; per-user role owner/manager/member; name unique per owner | |||
group_invitations | Gated group membership | `pending -> accepted \ | declined \ | revoked \ | expired`; ≤1 pending row per group/email; only invitee resolves it |
folders | Canonical Root Folder/subfolder classification, owned by one member | Roots unique per owner; siblings unique per parent (both case-insensitive, all statuses); owner_user_id/parent_folder_id/root_folder_id immutable; depth 0-32 | |||
files | Stable logical file identity | Opaque ID, owner, Root Folder, current version, archive state | |||
file_versions | Immutable content facts | Version number, opaque storage key, size, checksum, uploader, time | |||
file_metadata_revisions | Immutable classification history | Before/after values, actor, time, optional reason | |||
metadata_definitions | Controlled custom-field schema | Stable key; supported type and validation rules | |||
file_metadata_values | Validated current custom values | Each value conforms to its active definition | |||
access_grants | Direct or group access | Subject, file/Root Folder scope, level, grantor, lifecycle | |||
audit_events | Security and transaction accountability | Append-only and secret-free |
File identity
A logical file has one stable opaque ID. Uploading new bytes creates a new file_versions row and object; it does not replace the logical file or overwrite a prior object. The current version pointer advances only after the new object and catalog record are verified.
Changing name, Root Folder, description, custom metadata, or archive state modifies the logical file while also adding an immutable metadata revision. It does not create a content version.
Root Folder ownership and hierarchy
Every Root Folder has exactly one owner: the active member who created it (for a root) or that root's owner (for a subfolder, inherited transitively down the tree regardless of who ran the create command). Ownership implies manage on the Root Folder without a stored access_grants row -- the same "ownership implies manage, never a materialized grant" convention files already use. A root's trimmed name is unique case-insensitively per owner (active or deactivated); different owners may reuse the same name freely, since root uniqueness is scoped to ownership, not the whole organization.
A folder additionally carries a nullable parent_folder_id (null for a root), an immutable root_folder_id (equal to its own ID for a root, resolved once and never recomputed as the tree changes), and an integer depth (0 for a root, capped at 32). Parent, child, and root must share one organization and one root tree; a root cannot become another folder's child through the creation surface. A subfolder's trimmed name is unique case-insensitively among its immediate siblings -- active or deactivated -- independently of the organization-wide root-name scope above. Creating a subfolder requires the actor to hold live manage on the immediate parent at the moment of creation; a parent that is missing, cross-tree, or merely unmanageable is indistinguishable to the caller.
Group ownership, roles, and invitations
Every group is created by one active member, who becomes its sole owner; group_memberships carries a per-user, per-group role of owner, manager, or member rather than an undifferentiated flat list. A group must always retain at least one active owner -- no role change, member removal, or deactivation may leave it with zero. A trimmed group name is unique case-insensitively per owner (active or deactivated), independent of the organization-wide Root Folder name scope; a different owner may reuse the same name freely.
group_invitations gates membership: a pending row is created by an owner/manager inviting an existing active member, and only the invited user can move it to accepted or declined; an owner or manager can move it to revoked; the current time passing expires_at reads it as expired. At most one pending invitation may exist per (group, invited email) pair -- a resend refreshes that same row rather than inserting a duplicate. Inviting an email that does not resolve to an existing active member in the caller's organization returns the identical success shape a real invitation would, while inserting no row at all -- membership existence and organization membership are both anti-enumeration surfaces here, the same as file existence is elsewhere. Removing a member deletes only their group_memberships row; any access_grants row created for them directly (never through the group) is untouched and independently revocable.
Controlled metadata
Root Folder is a dedicated entity rather than a generic custom field. New files and metadata edits may reference only active Root Folders. Historical records remain readable when a Root Folder or custom field is deactivated.
Custom field types are limited to text, number, boolean, date, and choice. Field keys remain stable; labels, allowed choices, required state, and active state may evolve under server validation.
Constraint strategy
Database constraints enforce uniqueness, referential integrity, sequential relationships, and valid structural states where possible. Server-side authorization and workflow checks remain authoritative for rules involving identity, ownership, effective grants, storage verification, or cross-record policy.