Colony

RBAC & permissions

RBAC & permissions

Last updated 5/24/2026

Colony — RBAC & Permissions Documentation

Product: Colony — Command Interface for the ZoomProp GTM Agentic Stack Auth Provider: Clerk (Organizations, Users, RBAC) Document Status: v0.1 Spark → v0.6 Beta Last Updated: 2026-Q2


Table of Contents

  1. Overview & Design Principles
  2. Role Hierarchy
  3. Permission Matrix
  4. Business Rules (BR-XXX)
  5. Clerk Integration Architecture
  6. Middleware Configuration
  7. Protected Route Inventory
  8. Server-Side Auth Patterns
  9. Client-Side Auth Guards
  10. Row-Level Security
  11. API Key Vault & Scopes
  12. Webhook & Svix Auth
  13. Audit Logging
  14. CI Enforcement
  15. Open Items & Future Work

1. Overview & Design Principles

Colony enforces access control at three independent layers:

LayerMechanismEnforces
EdgeNext.js Middleware + Clerk auth()Route-level authentication; unauthenticated users never reach app logic
APIServer-side auth() / currentUser() + org-role checkAction-level authorization; role verified on every mutation
DataPostgreSQL RLS + org_id column predicateTenant isolation; cross-org data leakage impossible even with valid token

The authorization model is organization-scoped: every Colony tenant is a Clerk Organization. All resources (contacts, sequences, deals, recordings, knowledge entries, API keys) are owned by an org_id. A Clerk user can belong to multiple organizations and will carry the permissions of whichever organization's session is active (Clerk's active-org pattern).

Three invariants that must never be violated:

  1. No authenticated route may execute without a verified orgId in the Clerk session.
  2. No database query against a multi-tenant table may omit a WHERE org_id = $orgId predicate.
  3. No admin-only action may be authorized on the basis of member or viewer role alone.

2. Role Hierarchy

Colony implements three Clerk Organization roles. Roles are hierarchical — each level inherits all permissions of the roles below it.

┌─────────────────────────────────────────────────────────────────┐
│  org:admin                                                      │
│  Full control: billing, user management, vault, all agents,    │
│  circuit breakers, org settings, audit log read                │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │  org:member                                               │ │
│  │  Operational: run agents, manage outbound, approve        │ │
│  │  messages, manage deals/contacts/content/recordings,      │ │
│  │  read knowledge core, trigger briefs                      │ │
│  │                                                           │ │
│  │  ┌─────────────────────────────────────────────────────┐ │ │
│  │  │  org:viewer                                         │ │ │
│  │  │  Read-only: view pipeline, contacts, content,       │ │ │
│  │  │  recordings, briefs. No write, no agent trigger,    │ │ │
│  │  │  no vault access.                                   │ │ │
│  │  └─────────────────────────────────────────────────────┘ │ │
│  └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

2.1 Role Definitions

Clerk Role SlugDisplay NameDescription
org:adminAdministratorOrganization owner. Manages users, billing, API key vault, circuit breaker thresholds, and all operational capabilities.
org:memberMemberDay-to-day operator. Runs agents, manages outbound sequences, approves messages, manages CRM records, reads knowledge core.
org:viewerViewerRead-only observer. Views pipeline, contacts, content, recordings, and daily briefs. Cannot write, trigger agents, or access vault.

2.2 Role Assignment Rules

BR-001 — Every Clerk Organization must have at least one org:admin at all times. The last admin cannot downgrade their own role or be removed.

BR-002 — Users are assigned a role at organization invitation time. The default role for new invitations is org:member unless explicitly set to org:viewer or org:admin by an existing admin.

BR-003 — A user who is a Clerk platform superuser (ZoomProp internal) does not automatically receive org:admin in tenant organizations. Platform-level access and org-level access are separate.

BR-004 — Role changes take effect on the next Clerk session token issuance. Existing active sessions reflect the role at token-mint time. Admins requiring immediate role revocation must additionally invalidate the target user's sessions via Clerk's session management API.


3. Permission Matrix

The matrix below defines every resource × action pair and the minimum role required. ✅ = permitted, ❌ = denied, A = org:admin only, M = org:member and above, V = all authenticated users including org:viewer.

3.1 Core CRM Resources

ResourceActionviewermemberadmin
ContactsRead list / detail
ContactsCreate
ContactsUpdate
ContactsDelete
ContactsImport (CSV)
Contact ListsRead
Contact ListsCreate / Update / Delete
Deals (Pipeline)Read
DealsCreate / Update stage
DealsDelete
DealsBi-sync to Pipedrive

3.2 Outbound Engine

ResourceActionviewermemberadmin
SequencesRead
SequencesCreate / Update
SequencesDelete
SequencesEnroll contact
SequencesPause / Resume
Outbound MessagesRead queue
Outbound MessagesApprove / Edit
Outbound MessagesReject
HOT Reply QueueRead
HOT Reply QueueRespond
Circuit BreakersRead state
Circuit BreakersReset / Adjust threshold
Sender MailboxesRead
Sender MailboxesAdd / Remove / Configure

3.3 Agent Runtime

ResourceActionviewermemberadmin
GTM Orchestrator ChatRead history
GTM Orchestrator ChatSend message / trigger
Campaign OrchestratorTrigger campaign
Prospect AgentTrigger discovery
Prospect AgentApprove candidate
Qualification AgentRead scores
Qualification AgentOverride score
Message-Gen AgentGenerate draft
Message-Gen AgentApprove / Send
Inngest Event TriggersFire internal events✅ (platform-internal only)

3.4 Recording Intelligence

ResourceActionviewermemberadmin
RecordingsRead list / transcript
RecordingsIngest from Google Drive
RecordingsDelete
RecordingsExport signals to CRM
Post-Call SummariesRead
Post-Call SummariesRegenerate

3.5 Content Pipeline

ResourceActionviewermemberadmin
Content PiecesRead
Content PiecesCreate / Edit
Content PiecesDelete
Content PiecesPublish
Attribution DataRead

3.6 Knowledge Core

ResourceActionviewermemberadmin
Knowledge EntriesRead / Search
Knowledge EntriesCreate
Knowledge EntriesUpdate / Delete
pgvector EmbeddingsTrigger re-embed
Notion KC SyncTrigger sync

3.7 Onboarding & Deployment Kit

ResourceActionviewermemberadmin
Deployment KitRead assets
Deployment KitTrigger generation
Deployment KitDelete / Regenerate
Onboarding ChecklistRead
Onboarding ChecklistUpdate step status

3.8 Analytics & Daily Brief

ResourceActionviewermemberadmin
Daily BriefRead
Daily BriefTrigger manual generation
Analytics DashboardRead
Analytics DashboardExport data

3.9 Organization Administration

ResourceActionviewermemberadmin
Organization SettingsRead
Organization SettingsUpdate
Users / InvitationsRead member list
Users / InvitationsInvite user
Users / InvitationsChange role
Users / InvitationsRemove user
API Key VaultRead key names (masked)
API Key VaultCreate / Rotate / Delete
Audit LogRead
GCS Signed URLsGenerate✅ (scoped)
BillingRead / Manage

4. Business Rules (BR-XXX)

Access Control Invariants

BR-001 — An organization must maintain at least one org:admin. Removal of the last admin must be rejected with HTTP 422 and error code LAST_ADMIN_REMOVAL.

BR-002 — Default invitation role is org:member. Invitations may only set org:admin role if issued by an existing org:admin.

BR-003 — Clerk platform-level user status does not confer org:admin privileges within tenant organizations. All tenant access is gated on org membership.

BR-004 — Role changes apply on next token issuance. Immediate revocation requires explicit Clerk session invalidation in addition to role change.

BR-005 — All API routes must validate both userId (authenticated) and orgId (organization-scoped) from the Clerk session. A valid userId with no active orgId must be rejected with HTTP 403 and error code NO_ACTIVE_ORG.

BR-006 — Viewers (org:viewer) have no write access to any resource. Any write attempt by a viewer must return HTTP 403 with error code INSUFFICIENT_ROLE.

BR-007org:member users may not delete any resource. Deletes are exclusively org:admin actions.

BR-008org:member users may not access, create, rotate, or delete API vault keys (api_keys table). Vault access is exclusively org:admin.

BR-009 — Circuit breaker threshold adjustments are exclusively org:admin. Members may read circuit breaker state but cannot reset or reconfigure.

BR-010 — Sender mailbox configuration (add, remove, rotate credentials) is exclusively org:admin. Members operate within configured mailboxes.

BR-011 — Knowledge Core destructive operations (Update, Delete, Re-embed, Notion sync trigger) require org:admin. Members may create entries; viewers read only.

Tenant Isolation

BR-012 — Every database query against a multi-tenant table must include WHERE org_id = $orgId using the orgId extracted from the Clerk session. ORM or query-builder helpers must enforce this predicate as a default scope.

BR-013 — GCS signed URLs for asset access must be scoped to the requesting org's prefix (gs://colony-assets/{orgId}/…). Cross-org prefix access must be rejected.

BR-014 — The api_keys table rows are encrypted via GCP KMS with a per-org key. The KMS key reference must match the requesting org's configured key. Decryption using another org's KMS key must fail at the KMS layer.

BR-015 — Inngest event payloads must carry orgId as a top-level field. All Inngest step functions that access the database must extract orgId from the event payload—not from ambient context—and apply it as a query predicate.

Webhook Security

BR-016 — Svix webhook payloads from Clerk must be verified using the SVIX_WEBHOOK_SECRET before any user/org sync logic executes. Unverified payloads must be rejected with HTTP 401.

BR-017 — The Clerk webhook endpoint (/api/webhooks/clerk) must be excluded from Clerk middleware auth (it authenticates via Svix signature, not Clerk session token).

BR-018 — The Inngest serve endpoint (/api/inngest) must be excluded from Clerk middleware auth. Inngest authenticates requests using its own signing key.

Approval Workflow

BR-019 — Outbound message approval requires org:member or org:admin. An agent-generated message in pending_approval status must not be sent until a human with at least org:member role has approved it.

BR-020 — The auto-approve pathway (Phase 2 outreach pace auto-approve, see docs/phase2/action_plans/17_outreach_pace_auto_approve.md) must only activate when an org:admin has explicitly enabled the feature in organization settings. Auto-approve does not bypass the role requirement; the system acts on behalf of the org's configured approval delegation.

BR-021 — Candidate approval in the prospect queue requires org:member or org:admin. Viewers cannot approve, reject, or skip candidates.

Deployment Kit & Onboarding

BR-022 — Deployment Kit generation is triggered automatically on Closed-Won deal stage change. The triggering event carries the acting user's role. If triggered manually, the requesting user must be org:member or org:admin.

BR-023 — Deployment Kit assets are stored in GCS under the org's prefix. Signed URL generation for asset download requires the requesting user to be authenticated and a member of the owning org (any role).

Daily Brief & Analytics

BR-024 — Daily brief generation is a scheduled Inngest function with no interactive role check (it runs as a platform job). Manual re-trigger requires org:member or org:admin.

BR-025 — Analytics data export (CSV/JSON) requires org:member or org:admin. Viewers may view dashboards but cannot export raw data.

Org Switcher

BR-026 — When a user switches active organizations (Clerk org switcher), the new session token's orgId and orgRole must be used for all subsequent requests. Stale org context from prior sessions must not persist in client state. See docs/phase2/action_plans/27_org_switcher_hardening.md.

BR-027 — After an org switch, the client must invalidate all cached query results that are org-scoped before rendering any data from the new org.


5. Clerk Integration Architecture

5.1 Organization Roles Mapping

Clerk Organization roles map directly to Colony permission levels. No intermediate translation layer exists — the Clerk role slug is the authoritative permission token.

Clerk Org Role Slug        Colony Permission Level
─────────────────────────────────────────────────
org:admin              →   Full administrative access
org:member             →   Operational access (read + write, no admin)
org:viewer             →   Read-only access

5.2 Session Claims & Custom Metadata

Clerk issues JWT session tokens containing the following claims Colony depends on:

// Session token claims used by Colony
interface ColonySessionClaims {
  sub: string;           // Clerk userId (e.g., "user_2abc…")
  org_id: string;        // Active Clerk orgId (e.g., "org_2xyz…")
  org_role: string;      // "org:admin" | "org:member" | "org:viewer"
  org_slug: string;      // URL-safe org identifier
  org_permissions: string[];  // Clerk-issued fine-grained permissions (future)
}

Public Organization Metadata (set via Clerk Dashboard or Admin API, readable client-side):

{
  "plan": "growth",
  "features": ["multi_channel", "auto_approve"],
  "pipedrive_connected": true
}

Private Organization Metadata (set via Clerk Backend API only, server-side only):

{
  "kms_key_ref": "projects/colony-39989/locations/us-central1/keyRings/colony-org-keys/cryptoKeys/org_2xyz",
  "vault_initialized": true,
  "circuit_breaker_config": { "daily_limit": 200, "bounce_threshold": 0.05 }
}

Private User Metadata (server-side only):

{
  "onboarding_complete": true,
  "notification_prefs": { "daily_brief_email": true }
}

5.3 Clerk SDK Dependencies

// Expected in package.json
"@clerk/nextjs": "^5.x",
"@clerk/backend": "^1.x"

5.4 Environment Variables

VariablePurposeSecret Store
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYClient-side Clerk initGCP Secret Manager → Cloud Run env
CLERK_SECRET_KEYServer-side Clerk API callsGCP Secret Manager → Cloud Run env
CLERK_WEBHOOK_SECRETSvix webhook verificationGCP Secret Manager → Cloud Run env
NEXT_PUBLIC_CLERK_SIGN_IN_URLRedirect for unauthenticated users.env / Cloud Run env
NEXT_PUBLIC_CLERK_SIGN_UP_URLOrg creation flow entry.env / Cloud Run env
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URLPost-auth redirect target.env / Cloud Run env

6. Middleware Configuration

6.1 middleware.ts — Specification

The Next.js middleware runs at the edge on every request. It uses Clerk's clerkMiddleware (Next.js App Router pattern) to:

  1. Enforce authentication on all application routes.
  2. Allow unauthenticated access to Clerk's own auth routes, the Svix webhook endpoint, and the Inngest serve endpoint.
  3. Redirect unauthenticated users to the sign-in page.
// middleware.ts (at repository root, next to app/)
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

// Routes that must remain publicly accessible (no Clerk session required)
const isPublicRoute = createRouteMatcher([
  '/sign-in(.*)',
  '/sign-up(.*)',
  '/api/webhooks/clerk',   // BR-017: Svix-authenticated, not Clerk-session-authenticated
  '/api/inngest',          // BR-018: Inngest-authenticated
  '/_health',             // GCP Cloud Run health check
]);

export default clerkMiddleware(async (auth, req) => {
  if (!isPublicRoute(req)) {
    // Protect all non-public routes; redirects to NEXT_PUBLIC_CLERK_SIGN_IN_URL
    await auth.protect();
  }
});

export const config = {
  matcher: [
    // Run middleware on all routes except Next.js internals and static files
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    '/(api|trpc)(.*)',
  ],
};

6.2 Middleware Behavior by Route Class

Route PatternAuth RequiredOrg RequiredNotes
/sign-in(.*)Clerk hosted UI
/sign-up(.*)Clerk hosted UI
/api/webhooks/clerk❌ (Clerk)Svix HMAC — BR-016
/api/inngest❌ (Clerk)Inngest signing key — BR-018
/_healthGCP health probe
/overviewGTM chat interface
/outbound(.*)Outbound engine
/pipeline(.*)Deal tracker
/content(.*)Content pipeline
/recordings(.*)Recording intelligence
/knowledge(.*)Knowledge core
/onboarding(.*)Deployment kit
/analytics(.*)Analytics + briefs
/settings(.*)✅ (org:admin)Org administration
/api/(.*)All internal API routes

7. Protected Route Inventory

Every API route enforces authorization at two levels: (1) middleware ensures the user is authenticated, (2) the route handler verifies the org role satisfies the minimum requirement.

7.1 Route Authorization Helper

All API routes use a shared helper that extracts and validates the Clerk session:

// lib/auth/require-role.ts
import { auth } from '@clerk/nextjs/server';

type OrgRole = 'org:admin' | 'org:member' | 'org:viewer';

const ROLE_RANK: Record<OrgRole, number> = {
  'org:viewer': 0,
  'org:member': 1,
  'org:admin': 2,
};

export async function requireRole(minimumRole: OrgRole) {
  const { userId, orgId, orgRole } = await auth();

  // BR-005: Both userId and orgId must be present
  if (!userId || !orgId) {
    throw new AuthError('NO_ACTIVE_ORG', 403);
  }

  const userRank = ROLE_RANK[orgRole as OrgRole] ?? -1;
  const requiredRank = ROLE_RANK[minimumRole];

  // BR-006, BR-007, BR-008: Role hierarchy enforcement
  if (userRank < requiredRank) {
    throw new AuthError('INSUFFICIENT_ROLE', 403);
  }

  return { userId, orgId, orgRole: orgRole as OrgRole };
}

7.2 API Route Inventory

Webhook Routes (Public — Non-Clerk Auth)

RouteMethodAuth MechanismNotes
/api/webhooks/clerkPOSTSvix HMAC (CLERK_WEBHOOK_SECRET)BR-016, BR-017
/api/inngestGET, POSTInngest signing keyBR-018

Chat & Orchestrator

RouteMethodMin. RoleBR Refs
/api/chatGETorg:viewerBR-005
/api/chatPOST (stream)org:memberBR-005, BR-006
/api/chat/historyGETorg:viewerBR-005
/api/orchestrator/gtmPOSTorg:memberBR-005, BR-006
/api/orchestrator/campaignPOSTorg:memberBR-005, BR-006

Contacts

RouteMethodMin. RoleBR Refs
/api/contactsGETorg:viewerBR-005, BR-012
/api/contactsPOSTorg:memberBR-005, BR-006
/api/contacts/[id]GETorg:viewerBR-005, BR-012
/api/contacts/[id]PATCHorg:memberBR-005, BR-006
/api/contacts/[id]DELETEorg:adminBR-005, BR-007
/api/contacts/importPOSTorg:memberBR-005, BR-006
/api/contacts/listsGETorg:viewerBR-005, BR-012
/api/contacts/listsPOSTorg:memberBR-005, BR-006
/api/contacts/lists/[id]PATCHorg:memberBR-005, BR-006
/api/contacts/lists/[id]DELETEorg:adminBR-005, BR-007

Outbound & Sequences

RouteMethodMin. RoleBR Refs
/api/sequencesGETorg:viewerBR-005, BR-012
/api/sequencesPOSTorg:memberBR-005, BR-006
/api/sequences/[id]GETorg:viewerBR-005, BR-012
/api/sequences/[id]PATCHorg:memberBR-005, BR-006
/api/sequences/[id]DELETEorg:adminBR-005, BR-007
/api/sequences/[id]/enrollPOSTorg:memberBR-005, BR-006
/api/sequences/[id]/pausePOSTorg:memberBR-005, BR-006
/api/sequences/[id]/resumePOSTorg:memberBR-005, BR-006
/api/outbound/queueGETorg:viewerBR-005, BR-012
/api/outbound/approvePOSTorg:memberBR-005, BR-019
/api/outbound/rejectPOSTorg:memberBR-005, BR-019
/api/outbound/hot-repliesGETorg:viewerBR-005, BR-012
/api/outbound/hot-replies/[id]/respondPOSTorg:memberBR-005, BR-006
/api/circuit-breakersGETorg:viewerBR-005, BR-012
/api/circuit-breakers/[id]/resetPOSTorg:adminBR-005, BR-009
/api/circuit-breakers/[id]/thresholdPATCHorg:adminBR-005, BR-009
/api/mailboxesGETorg:viewerBR-005