-
Notifications
You must be signed in to change notification settings - Fork 1
1.1. Role‐Based Access Control (RBAC) Architecture
Benthara edited this page Oct 10, 2025
·
2 revisions
| Principle | Implementation Reflection |
|---|---|
| Privacy by Design | Platform-level roles (Admin, Engineer) cannot see private tenant/user content or identities unless explicit consent, legal audit, or system diagnostics require it. |
| Data Sovereignty | Each workspace/tenant owns its data — even the platform operator is only a processor, not a controller. |
| Unified Identity, Multi-Workspace Membership | Users have one master account, but can join multiple tenants with different roles (similar to how Slack, Notion, or GitHub orgs work). |
| Minimal Visibility Model | Platform Admins/Agents can see metadata (tenant name, health, billing status) but not content/data within the tenant. |
| Extensible Project Model | Projects can be of multiple types (GitCMS, PaperTrail, Docs, Sheets, Forms, Kanban, etc.), and workspaces (can be assign as tenants) act as containers (like Google Workspace → Drive folders). |
| Level | Role | Scope | Notes |
|---|---|---|---|
| 000 | Platform Admin | Global | Can manage system policies, plans, and compliance checks. Cannot see tenant content or user PII without legal override or consent. |
| 100 | Platform Engineer | Global | Handles system health, backups, performance. No visibility into tenant content or real user identities. |
| 200 | Tenant/Workspace Admin | Tenant/Workspace | Full control over that workspace — can manage members, billing, and content. |
| 300 | Admin | App/Workspace | Manages content and permissions within a workspace or project folder. |
| 400 | Editor | App | Create, edit, and publish content. No access to tenant settings. |
| 500 | Moderator | App | Manage contributions, handle reports/comments. |
| 600 | Contributor | App | Create drafts or personal content. |
| 700 | Viewer | App | Authenticated, read-only member of workspace or shared link. |
| 800 | Guest | App | Temporary or external viewer; may have read/comment privileges only when explicitly shared. |
| 950 | Automation/Bot | Service | Scoped per-token automation (backup, sync, CI/CD) with no human-equivalent privileges. |
(aligned with Privacy-by-Design and Workspace Federation)
Legend: ✅ full 🔸 limited (consent / scoped) ❌ none
| Capability | Platform Admin | Platform Engineer | Tenant Admin | Admin | Editor | Moderator | Contributor | Viewer | Guest | Bot |
|---|---|---|---|---|---|---|---|---|---|---|
| Platform Settings & Policies | ✅ | 🔸 (ops configs) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Tenant Lifecycle (create/suspend/delete) | ✅ | 🔸 (technical actions only) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| System Health & Monitoring (non-PII) | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| View Tenant Metadata (name, plan, region) | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| View Tenant Member Identities (PII) | 🔸 (only anonymized or compliance-approved) | ❌ | ✅ | 🔸 (within scope) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| View Tenant Content | 🔸 (aggregated, anonymized analytics only) | ❌ | ✅ | ✅ | ✅ | 🔸 (flagged/reports) | 🔸 (own) | ❌ | ❌ | 🔸 (per token) |
| Modify Tenant Content | ❌ | ❌ | ✅ | ✅ | ✅ | 🔸 (moderation) | 🔸 (own only) | ❌ | ❌ | 🔸 |
| Access Private Workspaces | 🔸 (via explicit legal/compliance override) | ❌ | ✅ | ✅ | ✅ | ✅ | 🔸 (own only) | ❌ | ❌ | 🔸 |
| Cross-Tenant Access | ❌ | ❌ | ❌ | ✅ (if invited to multiple workspaces) | ✅ | ✅ | ✅ | 🔸 (shared links) | 🔸 | 🔸 |
| User & Role Management (within workspace) | 🔸 (consent-based audit only) | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Project Management (create / delete projects) | ❌ | ❌ | ✅ | ✅ | 🔸 | ❌ | ❌ | ❌ | ❌ | 🔸 |
| Project Types (GitCMS, Docs, Sheets, etc.) | ❌ | ❌ | ✅ | ✅ | ✅ | 🔸 | 🔸 | 🔸 | ❌ | ✅ (per type) |
| Comment / Collaboration Tools | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🔸 | ❌ |
| Moderation / Reporting | ❌ | ❌ | ✅ | ✅ | 🔸 | ✅ | ❌ | ❌ | ❌ | ❌ |
| Audit Logs (tenant) | 🔸 (aggregated) | 🔸 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | 🔸 |
| Audit Logs (platform) | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Integrations & API Tokens (workspace scope) | 🔸 (manage scopes, not data) | ✅ (infra only) | ✅ | ✅ | 🔸 | ❌ | ❌ | ❌ | ❌ | ✅ |
| Billing / Subscription | ✅ | 🔸 (system health only) | ✅ | 🔸 (view only) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| System Maintenance / Deployments | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| Data Export / Portability | 🔸 (compliance process) | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ (backups only) |
| Data Deletion (tenant scope) | 🔸 (compliance verified) | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Read Public Content | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
- Cannot view or alter tenant data or real user identities
only system metadata, resource utilization, and anonymized aggregates.
- PII access requires explicit “Compliance Mode” flag (logged + auditable).
- Engineering visibility limited to telemetry and encrypted payload sizes — not decrypted data.
- Full sovereignty: tenant owns its workspace and can self-manage users.
- Admins can invite cross-org users via identity links (not new accounts).
- User identities are unified globally (one profile → many workspace memberships).
- Bots must use scoped tokens (capabilities embedded in token claims).
Example scopes:
content:read,backup:run,sync:drive,ci:deploy.
- Compliance Auditor Role (optional) – read-only audit access to logs + encrypted data footprints.
- Workspace Federation Layer – allow peer-to-peer workspace links (shared projects between orgs).
- Consent Ledger – every access granted to platform staff logged with justification & expiry.
{
"meta": {
"version": "2.0",
"capability_value_semantics": ["allow", "deny", "consent", "compliance", "scoped", "anonymized"],
"notes": "Privacy-by-design: platform roles have no tenant content/PII access by default; PI/PII access requires consent or compliance override and is fully audited."
},
"capabilities_catalog": [
{ "key": "platform_settings", "description": "Manage platform-wide settings, policies, and feature flags." },
{ "key": "tenant_lifecycle", "description": "Create/suspend/delete tenants or workspaces." },
{ "key": "system_health_monitoring", "description": "View platform health/telemetry without tenant data visibility." },
{ "key": "view_tenant_metadata", "description": "View tenant/workspace metadata (name, plan, region)." },
{ "key": "view_member_identities", "description": "View real user identities and emails within a tenant/workspace." },
{ "key": "view_content_private", "description": "View private tenant content (drafts, non-public data)." },
{ "key": "modify_content", "description": "Create/edit/publish/delete tenant content." },
{ "key": "access_private_workspaces", "description": "Enter private workspaces not explicitly shared." },
{ "key": "cross_tenant_access", "description": "Participate across multiple tenants via memberships." },
{ "key": "manage_workspace_users_roles", "description": "Invite members and assign roles within a tenant/workspace." },
{ "key": "project_manage", "description": "Create/archive/delete projects within a workspace." },
{ "key": "project_types_manage", "description": "Enable/manage project types (GitCMS, Docs, Sheets, Forms, Kanban, PaperTrail, etc.)." },
{ "key": "comment_collaborate", "description": "Comment, review, react, collaborate in-app." },
{ "key": "moderate_review", "description": "Moderate submissions, handle reports, approve/reject." },
{ "key": "audit_logs_tenant", "description": "View tenant-scoped audit logs." },
{ "key": "audit_logs_platform", "description": "View platform-scoped audit logs." },
{ "key": "integrations_api_tokens", "description": "Create/manage integrations and API tokens (workspace scope)." },
{ "key": "billing_subscription", "description": "Manage billing, subscriptions, and invoices." },
{ "key": "system_maintenance", "description": "Perform deployments, backups, restores, migrations." },
{ "key": "data_export_portability", "description": "Export workspace data (portability)." },
{ "key": "data_deletion_tenant", "description": "Delete tenant/workspace data." },
{ "key": "read_public_content", "description": "Read publicly shared content." },
{ "key": "aggregated_analytics", "description": "Access anonymized/aggregate analytics only (no PII)." },
{ "key": "compliance_override_access", "description": "Activate compliance/legal override for time-boxed, audited access." },
{ "key": "token_scopes_manage", "description": "Define and assign fine-grained token scopes (least privilege)." }
],
"roles": [
{
"id": 0,
"key": "platform_admin",
"label": "Platform Admin",
"level": 0,
"scope": "global",
"description": "Full platform governance without default access to tenant content or identities.",
"capabilities": {
"platform_settings": "allow",
"tenant_lifecycle": "allow",
"system_health_monitoring": "allow",
"view_tenant_metadata": "allow",
"aggregated_analytics": "anonymized",
"view_member_identities": "compliance",
"view_content_private": "compliance",
"modify_content": "deny",
"access_private_workspaces": "compliance",
"cross_tenant_access": "deny",
"manage_workspace_users_roles": "consent",
"project_manage": "deny",
"project_types_manage": "deny",
"comment_collaborate": "deny",
"moderate_review": "deny",
"audit_logs_tenant": "anonymized",
"audit_logs_platform": "allow",
"integrations_api_tokens": "allow",
"billing_subscription": "allow",
"system_maintenance": "allow",
"data_export_portability": "compliance",
"data_deletion_tenant": "compliance",
"read_public_content": "allow",
"compliance_override_access": "allow",
"token_scopes_manage": "allow"
}
},
{
"id": 1,
"key": "platform_engineer",
"label": "Platform Engineer",
"level": 100,
"scope": "global",
"description": "Operations & reliability; no tenant data/PII by default.",
"capabilities": {
"platform_settings": "deny",
"tenant_lifecycle": "consent",
"system_health_monitoring": "allow",
"view_tenant_metadata": "allow",
"aggregated_analytics": "anonymized",
"view_member_identities": "deny",
"view_content_private": "deny",
"modify_content": "deny",
"access_private_workspaces": "deny",
"cross_tenant_access": "deny",
"manage_workspace_users_roles": "deny",
"project_manage": "deny",
"project_types_manage": "deny",
"comment_collaborate": "deny",
"moderate_review": "deny",
"audit_logs_tenant": "anonymized",
"audit_logs_platform": "allow",
"integrations_api_tokens": "allow",
"billing_subscription": "deny",
"system_maintenance": "allow",
"data_export_portability": "deny",
"data_deletion_tenant": "deny",
"read_public_content": "allow",
"compliance_override_access": "consent",
"token_scopes_manage": "allow"
}
},
{
"id": 2,
"key": "tenant_admin",
"label": "Tenant Admin",
"level": 200,
"scope": "tenant",
"description": "Owns a workspace/tenant; full control within that boundary.",
"capabilities": {
"platform_settings": "deny",
"tenant_lifecycle": "deny",
"system_health_monitoring": "deny",
"view_tenant_metadata": "allow",
"aggregated_analytics": "allow",
"view_member_identities": "allow",
"view_content_private": "allow",
"modify_content": "allow",
"access_private_workspaces": "allow",
"cross_tenant_access": "allow",
"manage_workspace_users_roles": "allow",
"project_manage": "allow",
"project_types_manage": "allow",
"comment_collaborate": "allow",
"moderate_review": "allow",
"audit_logs_tenant": "allow",
"audit_logs_platform": "deny",
"integrations_api_tokens": "allow",
"billing_subscription": "allow",
"system_maintenance": "deny",
"data_export_portability": "allow",
"data_deletion_tenant": "allow",
"read_public_content": "allow",
"compliance_override_access": "deny",
"token_scopes_manage": "allow"
}
},
{
"id": 3,
"key": "admin",
"label": "Admin",
"level": 300,
"scope": "tenant",
"description": "Top admin within a workspace’s apps and projects.",
"capabilities": {
"platform_settings": "deny",
"tenant_lifecycle": "deny",
"system_health_monitoring": "deny",
"view_tenant_metadata": "allow",
"aggregated_analytics": "allow",
"view_member_identities": "consent",
"view_content_private": "allow",
"modify_content": "allow",
"access_private_workspaces": "allow",
"cross_tenant_access": "allow",
"manage_workspace_users_roles": "allow",
"project_manage": "allow",
"project_types_manage": "allow",
"comment_collaborate": "allow",
"moderate_review": "allow",
"audit_logs_tenant": "allow",
"audit_logs_platform": "deny",
"integrations_api_tokens": "allow",
"billing_subscription": "consent",
"system_maintenance": "deny",
"data_export_portability": "allow",
"data_deletion_tenant": "consent",
"read_public_content": "allow",
"compliance_override_access": "deny",
"token_scopes_manage": "allow"
}
},
{
"id": 4,
"key": "editor",
"label": "Editor",
"level": 400,
"scope": "tenant",
"description": "Create, edit, and publish within assigned projects.",
"capabilities": {
"platform_settings": "deny",
"tenant_lifecycle": "deny",
"system_health_monitoring": "deny",
"view_tenant_metadata": "allow",
"aggregated_analytics": "allow",
"view_member_identities": "deny",
"view_content_private": "allow",
"modify_content": "allow",
"access_private_workspaces": "allow",
"cross_tenant_access": "allow",
"manage_workspace_users_roles": "deny",
"project_manage": "consent",
"project_types_manage": "deny",
"comment_collaborate": "allow",
"moderate_review": "consent",
"audit_logs_tenant": "deny",
"audit_logs_platform": "deny",
"integrations_api_tokens": "consent",
"billing_subscription": "deny",
"system_maintenance": "deny",
"data_export_portability": "consent",
"data_deletion_tenant": "deny",
"read_public_content": "allow",
"compliance_override_access": "deny",
"token_scopes_manage": "deny"
}
},
{
"id": 5,
"key": "moderator",
"label": "Moderator",
"level": 500,
"scope": "tenant",
"description": "Moderation and abuse handling within scope.",
"capabilities": {
"platform_settings": "deny",
"tenant_lifecycle": "deny",
"system_health_monitoring": "deny",
"view_tenant_metadata": "allow",
"aggregated_analytics": "allow",
"view_member_identities": "deny",
"view_content_private": "consent",
"modify_content": "consent",
"access_private_workspaces": "allow",
"cross_tenant_access": "allow",
"manage_workspace_users_roles": "deny",
"project_manage": "deny",
"project_types_manage": "deny",
"comment_collaborate": "allow",
"moderate_review": "allow",
"audit_logs_tenant": "consent",
"audit_logs_platform": "deny",
"integrations_api_tokens": "deny",
"billing_subscription": "deny",
"system_maintenance": "deny",
"data_export_portability": "deny",
"data_deletion_tenant": "deny",
"read_public_content": "allow",
"compliance_override_access": "deny",
"token_scopes_manage": "deny"
}
},
{
"id": 6,
"key": "contributor",
"label": "Contributor",
"level": 600,
"scope": "tenant",
"description": "Create/edit own drafts; submit for review.",
"capabilities": {
"platform_settings": "deny",
"tenant_lifecycle": "deny",
"system_health_monitoring": "deny",
"view_tenant_metadata": "allow",
"aggregated_analytics": "deny",
"view_member_identities": "deny",
"view_content_private": "consent",
"modify_content": "consent",
"access_private_workspaces": "allow",
"cross_tenant_access": "allow",
"manage_workspace_users_roles": "deny",
"project_manage": "deny",
"project_types_manage": "deny",
"comment_collaborate": "allow",
"moderate_review": "deny",
"audit_logs_tenant": "deny",
"audit_logs_platform": "deny",
"integrations_api_tokens": "deny",
"billing_subscription": "deny",
"system_maintenance": "deny",
"data_export_portability": "deny",
"data_deletion_tenant": "deny",
"read_public_content": "allow",
"compliance_override_access": "deny",
"token_scopes_manage": "deny"
}
},
{
"id": 7,
"key": "viewer",
"label": "Viewer",
"level": 700,
"scope": "tenant",
"description": "Authenticated read-only member.",
"capabilities": {
"platform_settings": "deny",
"tenant_lifecycle": "deny",
"system_health_monitoring": "deny",
"view_tenant_metadata": "allow",
"aggregated_analytics": "deny",
"view_member_identities": "deny",
"view_content_private": "deny",
"modify_content": "deny",
"access_private_workspaces": "allow",
"cross_tenant_access": "allow",
"manage_workspace_users_roles": "deny",
"project_manage": "deny",
"project_types_manage": "deny",
"comment_collaborate": "allow",
"moderate_review": "deny",
"audit_logs_tenant": "deny",
"audit_logs_platform": "deny",
"integrations_api_tokens": "deny",
"billing_subscription": "deny",
"system_maintenance": "deny",
"data_export_portability": "deny",
"data_deletion_tenant": "deny",
"read_public_content": "allow",
"compliance_override_access": "deny",
"token_scopes_manage": "deny"
}
},
{
"id": 8,
"key": "guest",
"label": "Guest",
"level": 800,
"scope": "tenant",
"description": "Temporary/external viewer; minimal access.",
"capabilities": {
"platform_settings": "deny",
"tenant_lifecycle": "deny",
"system_health_monitoring": "deny",
"view_tenant_metadata": "consent",
"aggregated_analytics": "deny",
"view_member_identities": "deny",
"view_content_private": "deny",
"modify_content": "deny",
"access_private_workspaces": "deny",
"cross_tenant_access": "deny",
"manage_workspace_users_roles": "deny",
"project_manage": "deny",
"project_types_manage": "deny",
"comment_collaborate": "consent",
"moderate_review": "deny",
"audit_logs_tenant": "deny",
"audit_logs_platform": "deny",
"integrations_api_tokens": "deny",
"billing_subscription": "deny",
"system_maintenance": "deny",
"data_export_portability": "deny",
"data_deletion_tenant": "deny",
"read_public_content": "allow",
"compliance_override_access": "deny",
"token_scopes_manage": "deny"
}
},
{
"id": 9,
"key": "automation_bot",
"label": "Automation / Bot",
"level": 950,
"scope": "service",
"description": "Non-human principal; all access must be token-scoped and least-privilege.",
"capabilities": {
"platform_settings": "deny",
"tenant_lifecycle": "deny",
"system_health_monitoring": "scoped",
"view_tenant_metadata": "scoped",
"aggregated_analytics": "scoped",
"view_member_identities": "deny",
"view_content_private": "scoped",
"modify_content": "scoped",
"access_private_workspaces": "scoped",
"cross_tenant_access": "deny",
"manage_workspace_users_roles": "deny",
"project_manage": "scoped",
"project_types_manage": "deny",
"comment_collaborate": "deny",
"moderate_review": "deny",
"audit_logs_tenant": "scoped",
"audit_logs_platform": "deny",
"integrations_api_tokens": "scoped",
"billing_subscription": "deny",
"system_maintenance": "scoped",
"data_export_portability": "scoped",
"data_deletion_tenant": "deny",
"read_public_content": "allow",
"compliance_override_access": "deny",
"token_scopes_manage": "scoped"
}
}
]
}-- =========================================================
-- EXTENSIONS
-- =========================================================
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE EXTENSION IF NOT EXISTS "citext";
-- =========================================================
-- ENUMS
-- =========================================================
CREATE TYPE role_scope AS ENUM ('global', 'tenant', 'service');
CREATE TYPE user_type AS ENUM ('human', 'bot');
CREATE TYPE workspace_type AS ENUM ('organization', 'personal');
CREATE TYPE membership_status AS ENUM ('active', 'invited', 'suspended');
CREATE TYPE capability_value AS ENUM ('allow', 'deny', 'consent', 'compliance', 'scoped', 'anonymized');
CREATE TYPE principal_type AS ENUM ('user', 'membership'); -- for tokens
CREATE TYPE project_type AS ENUM ('workspace', 'gitcms', 'papertrail', 'docs', 'sheets', 'forms', 'kanban', 'other');
CREATE TYPE share_permission AS ENUM ('view', 'comment', 'edit', 'publish', 'admin'); -- for links
CREATE TYPE audit_channel AS ENUM ('platform', 'tenant', 'project');
CREATE TYPE consent_subject_type AS ENUM ('user', 'membership', 'project', 'tenant');
CREATE TYPE compliance_reason AS ENUM ('law_enforcement', 'legal_hold', 'data_export', 'incident_response', 'other');
-- =========================================================
-- USERS & IDENTITY
-- =========================================================
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_type user_type NOT NULL DEFAULT 'human',
username CITEXT UNIQUE,
display_name TEXT,
picture_url TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
primary_email_id UUID, -- set after first email verified
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT users_primary_email_fk
FOREIGN KEY (primary_email_id) REFERENCES user_emails(id) DEFERRABLE INITIALLY DEFERRED
);
CREATE TABLE user_emails (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
email CITEXT NOT NULL,
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
verified_at TIMESTAMPTZ,
UNIQUE (email),
UNIQUE (user_id, email)
);
-- Optional: per-org aliases (if an org assigns an alias email to the same user)
CREATE TABLE user_email_aliases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL,
alias CITEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, alias)
);
CREATE TABLE user_profiles (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
locale TEXT,
timezone TEXT,
meta JSONB NOT NULL DEFAULT '{}'
);
-- =========================================================
-- WORKSPACES / TENANTS
-- =========================================================
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug CITEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
workspace_type workspace_type NOT NULL DEFAULT 'organization',
plan TEXT DEFAULT 'free',
region TEXT, -- e.g., 'eu-central-1'
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_by UUID REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX tenants_active_idx ON tenants(is_active);
-- =========================================================
-- ROLES & CAPABILITIES (privacy semantics)
-- =========================================================
CREATE TABLE roles (
id SMALLINT PRIMARY KEY, -- 0..999
key TEXT UNIQUE NOT NULL, -- 'platform_admin', ...
label TEXT NOT NULL,
level INTEGER NOT NULL CHECK (level BETWEEN 0 AND 999),
scope role_scope NOT NULL, -- global | tenant | service
description TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE capabilities (
key TEXT PRIMARY KEY,
description TEXT NOT NULL
);
-- capability_value follows your v2 semantics
CREATE TABLE role_capabilities (
role_id SMALLINT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
capability_key TEXT NOT NULL REFERENCES capabilities(key) ON DELETE CASCADE,
value capability_value NOT NULL DEFAULT 'deny',
PRIMARY KEY (role_id, capability_key)
);
-- =========================================================
-- MEMBERSHIPS (user in tenant) + ROLE ASSIGNMENTS
-- =========================================================
CREATE TABLE memberships (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
status membership_status NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (user_id, tenant_id)
);
CREATE INDEX memberships_user_idx ON memberships(user_id);
CREATE INDEX memberships_tenant_idx ON memberships(tenant_id);
CREATE TABLE membership_roles (
membership_id UUID NOT NULL REFERENCES memberships(id) ON DELETE CASCADE,
role_id SMALLINT NOT NULL REFERENCES roles(id) ON DELETE RESTRICT,
PRIMARY KEY (membership_id, role_id)
);
-- Global roles (platform scope)
CREATE TABLE user_global_roles (
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role_id SMALLINT NOT NULL REFERENCES roles(id) ON DELETE RESTRICT,
PRIMARY KEY (user_id, role_id),
CONSTRAINT ugr_role_scope_global CHECK (
(SELECT scope FROM roles WHERE roles.id = role_id) = 'global'
)
);
-- =========================================================
-- CONSENT LEDGER & COMPLIANCE OVERRIDES
-- =========================================================
-- Records explicit grant of consent by a controller (tenant) or subject (user).
CREATE TABLE consent_ledger (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
subject_type consent_subject_type NOT NULL, -- user | membership | project | tenant
subject_id UUID NOT NULL, -- FK semantic (not enforced across multiple tables)
capability_key TEXT NOT NULL REFERENCES capabilities(key) ON DELETE RESTRICT,
granted_by_user UUID REFERENCES users(id) ON DELETE SET NULL,
reason TEXT,
starts_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ, -- must be time-boxed ideally
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX consent_ledger_tenant_idx ON consent_ledger(tenant_id, capability_key);
CREATE INDEX consent_ledger_subject_idx ON consent_ledger(subject_id, subject_type);
CREATE INDEX consent_ledger_expiry_idx ON consent_ledger(expires_at);
-- Compliance override requires heavy auditing and justification
CREATE TABLE compliance_overrides (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL, -- typically Platform Admin
reason_code compliance_reason NOT NULL,
reason_detail TEXT,
capability_key TEXT NOT NULL REFERENCES capabilities(key) ON DELETE RESTRICT,
scope_filter JSONB NOT NULL DEFAULT '{}'::jsonb, -- e.g., {"project_id": "..."}
starts_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL, -- must be time-boxed
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX compliance_overrides_tenant_idx ON compliance_overrides(tenant_id, capability_key, expires_at);
-- =========================================================
-- API TOKENS (scoped; user or membership principals)
-- =========================================================
CREATE TABLE api_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
principal_type principal_type NOT NULL, -- user or membership
principal_id UUID NOT NULL, -- users.id or memberships.id
tenant_id UUID, -- populated if principal=membership (redundant but helpful)
name TEXT NOT NULL,
hash TEXT NOT NULL, -- hashed token
scopes JSONB NOT NULL DEFAULT '[]', -- e.g., ["content:read","backup:run"]
claims JSONB NOT NULL DEFAULT '{}', -- additional claims (capability gates)
expires_at TIMESTAMPTZ,
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK ((tenant_id IS NULL) OR (principal_type = 'membership'))
);
CREATE INDEX api_tokens_principal_idx ON api_tokens(principal_type, principal_id);
CREATE INDEX api_tokens_tenant_idx ON api_tokens(tenant_id);
CREATE INDEX api_tokens_last_used_idx ON api_tokens(last_used_at);
-- =========================================================
-- AUDIT LOGS (PII-safe; event metadata only)
-- =========================================================
CREATE TABLE audit_logs (
id BIGSERIAL PRIMARY KEY,
channel audit_channel NOT NULL, -- platform | tenant | project
tenant_id UUID,
project_id UUID,
actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
actor_token_id UUID REFERENCES api_tokens(id) ON DELETE SET NULL,
action TEXT NOT NULL, -- e.g., 'content.publish'
target_table TEXT,
target_id TEXT,
ip INET,
user_agent TEXT,
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX audit_logs_tenant_time_idx ON audit_logs(tenant_id, created_at DESC);
CREATE INDEX audit_logs_actor_time_idx ON audit_logs(actor_user_id, created_at DESC);
CREATE INDEX audit_logs_channel_time_idx ON audit_logs(channel, created_at DESC);
-- =========================================================
-- PROJECTS & PERMISSIONS (workspace folders + apps)
-- =========================================================
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
parent_id UUID REFERENCES projects(id) ON DELETE CASCADE, -- folders/hierarchy
type project_type NOT NULL,
key CITEXT, -- optional short code (e.g., "OPS", "SITE")
name TEXT NOT NULL,
description TEXT,
is_archived BOOLEAN NOT NULL DEFAULT FALSE,
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, key)
);
CREATE INDEX projects_tenant_idx ON projects(tenant_id);
-- Optional: per-project role overrides (default is tenant membership roles)
CREATE TABLE project_memberships (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
membership_id UUID NOT NULL REFERENCES memberships(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (project_id, membership_id)
);
CREATE TABLE project_membership_roles (
project_membership_id UUID NOT NULL REFERENCES project_memberships(id) ON DELETE CASCADE,
role_id SMALLINT NOT NULL REFERENCES roles(id) ON DELETE RESTRICT,
PRIMARY KEY (project_membership_id, role_id)
);
-- =========================================================
-- SHARING (PUBLIC/EXTERNAL LINKS)
-- =========================================================
CREATE TABLE share_links (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
target_table TEXT, -- e.g., 'documents'
target_id UUID, -- referenced entity id (nullable if project-level)
token_hash TEXT NOT NULL, -- hashed secret
permission share_permission NOT NULL DEFAULT 'view',
expires_at TIMESTAMPTZ,
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX share_links_project_idx ON share_links(project_id);
CREATE INDEX share_links_target_idx ON share_links(target_table, target_id);
-- =========================================================
-- VIEWS (effective roles & privacy-aware projections)
-- =========================================================
-- Effective tenant roles for a user (via membership)
CREATE VIEW v_effective_membership_roles AS
SELECT
m.user_id,
m.tenant_id,
r.id AS role_id,
r.key AS role_key,
r.level AS role_level,
r.scope AS role_scope
FROM memberships m
JOIN membership_roles mr ON mr.membership_id = m.id
JOIN roles r ON r.id = mr.role_id
WHERE m.status = 'active';
-- Effective project roles (if project override exists; else fall back to tenant roles at query time)
CREATE VIEW v_effective_project_roles AS
SELECT
pm.membership_id,
pm.project_id,
r.id AS role_id,
r.key AS role_key,
r.level AS role_level
FROM project_memberships pm
JOIN project_membership_roles pmr ON pmr.project_membership_id = pm.id
JOIN roles r ON r.id = pmr.role_id;
-- Platform/global roles for a user
CREATE VIEW v_effective_global_roles AS
SELECT
ugr.user_id,
r.id AS role_id,
r.key AS role_key,
r.level AS role_level
FROM user_global_roles ugr
JOIN roles r ON r.id = ugr.role_id
WHERE r.scope = 'global';
-- Consent currently in force (not expired)
CREATE VIEW v_active_consents AS
SELECT *
FROM consent_ledger
WHERE expires_at IS NULL OR expires_at > now();
-- Compliance overrides currently in force
CREATE VIEW v_active_compliance_overrides AS
SELECT *
FROM compliance_overrides
WHERE expires_at > now();
-- =========================================================
-- INDEXING ESSENTIALS (already sprinkled above)
-- =========================================================-
Unified Identity, Multi-Email:
users ←→ user_emails(verified; primary). You can map org aliases viauser_email_aliaseswithout duplicating users. -
Workspaces / Tenants:
tenants.workspace_typesupports organization and personal workspaces (single user, shareable later). -
RBAC with Privacy Semantics:
roles + capabilities + role_capabilities.value(enum) encode allow/deny/consent/compliance/scoped/anonymized. -
Consent & Compliance:
-
consent_ledgerrecords explicit, time-boxed consent (by tenant admin or subject). -
compliance_overridesis heavy-duty: reason code, JSON scope filters, hard expiry—must be audited.
-
-
Tokens (Bots & Integrations):
api_tokenswith principal (userormembership) + scopes/claims for least-privilege automation. Tie a bot to a tenant by creating a bot users row, then a memberships row, and mint a token with tenant scope. -
Projects & Project Types:
projectssupports foldering (parent_id), types (GitCMS,PaperTrail,Docs,Sheets,Forms,Kanban,Workspace). Optional per-project overrides viaproject_memberships+project_membership_roles. -
Share Links:
share_linksgive granular view/comment/edit/publish/admin access with tokenized, time-boxed links (no account needed). -
Auditing:
audit_logschannelized (platform/tenant/project). Avoid storing PII in logs; keep IDs + metadata.
Enable RLS on tenant-scoped tables (memberships, projects, content tables, etc.). Use JWT claims or SET LOCAL app.current_principal to evaluate:
- tenant access via membership
- project overrides via
v_effective_project_roles - capability resolution that respects
consent_ledgerandcompliance_overrides - platform roles can never bypass RLS except when a compliance override record exists (checked by a policy function).