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
- Overview & Design Principles
- Role Hierarchy
- Permission Matrix
- Business Rules (BR-XXX)
- Clerk Integration Architecture
- Middleware Configuration
- Protected Route Inventory
- Server-Side Auth Patterns
- Client-Side Auth Guards
- Row-Level Security
- API Key Vault & Scopes
- Webhook & Svix Auth
- Audit Logging
- CI Enforcement
- Open Items & Future Work
1. Overview & Design Principles
Colony enforces access control at three independent layers:
| Layer | Mechanism | Enforces |
|---|---|---|
| Edge | Next.js Middleware + Clerk auth() | Route-level authentication; unauthenticated users never reach app logic |
| API | Server-side auth() / currentUser() + org-role check | Action-level authorization; role verified on every mutation |
| Data | PostgreSQL RLS + org_id column predicate | Tenant 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:
- No authenticated route may execute without a verified
orgIdin the Clerk session. - No database query against a multi-tenant table may omit a
WHERE org_id = $orgIdpredicate. - No
admin-only action may be authorized on the basis ofmemberorviewerrole 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 Slug | Display Name | Description |
|---|---|---|
org:admin | Administrator | Organization owner. Manages users, billing, API key vault, circuit breaker thresholds, and all operational capabilities. |
org:member | Member | Day-to-day operator. Runs agents, manages outbound sequences, approves messages, manages CRM records, reads knowledge core. |
org:viewer | Viewer | Read-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
| Resource | Action | viewer | member | admin |
|---|---|---|---|---|
| Contacts | Read list / detail | ✅ | ✅ | ✅ |
| Contacts | Create | ❌ | ✅ | ✅ |
| Contacts | Update | ❌ | ✅ | ✅ |
| Contacts | Delete | ❌ | ❌ | ✅ |
| Contacts | Import (CSV) | ❌ | ✅ | ✅ |
| Contact Lists | Read | ✅ | ✅ | ✅ |
| Contact Lists | Create / Update / Delete | ❌ | ✅ | ✅ |
| Deals (Pipeline) | Read | ✅ | ✅ | ✅ |
| Deals | Create / Update stage | ❌ | ✅ | ✅ |
| Deals | Delete | ❌ | ❌ | ✅ |
| Deals | Bi-sync to Pipedrive | ❌ | ✅ | ✅ |
3.2 Outbound Engine
| Resource | Action | viewer | member | admin |
|---|---|---|---|---|
| Sequences | Read | ✅ | ✅ | ✅ |
| Sequences | Create / Update | ❌ | ✅ | ✅ |
| Sequences | Delete | ❌ | ❌ | ✅ |
| Sequences | Enroll contact | ❌ | ✅ | ✅ |
| Sequences | Pause / Resume | ❌ | ✅ | ✅ |
| Outbound Messages | Read queue | ✅ | ✅ | ✅ |
| Outbound Messages | Approve / Edit | ❌ | ✅ | ✅ |
| Outbound Messages | Reject | ❌ | ✅ | ✅ |
| HOT Reply Queue | Read | ✅ | ✅ | ✅ |
| HOT Reply Queue | Respond | ❌ | ✅ | ✅ |
| Circuit Breakers | Read state | ✅ | ✅ | ✅ |
| Circuit Breakers | Reset / Adjust threshold | ❌ | ❌ | ✅ |
| Sender Mailboxes | Read | ✅ | ✅ | ✅ |
| Sender Mailboxes | Add / Remove / Configure | ❌ | ❌ | ✅ |
3.3 Agent Runtime
| Resource | Action | viewer | member | admin |
|---|---|---|---|---|
| GTM Orchestrator Chat | Read history | ✅ | ✅ | ✅ |
| GTM Orchestrator Chat | Send message / trigger | ❌ | ✅ | ✅ |
| Campaign Orchestrator | Trigger campaign | ❌ | ✅ | ✅ |
| Prospect Agent | Trigger discovery | ❌ | ✅ | ✅ |
| Prospect Agent | Approve candidate | ❌ | ✅ | ✅ |
| Qualification Agent | Read scores | ✅ | ✅ | ✅ |
| Qualification Agent | Override score | ❌ | ✅ | ✅ |
| Message-Gen Agent | Generate draft | ❌ | ✅ | ✅ |
| Message-Gen Agent | Approve / Send | ❌ | ✅ | ✅ |
| Inngest Event Triggers | Fire internal events | ❌ | ❌ | ✅ (platform-internal only) |
3.4 Recording Intelligence
| Resource | Action | viewer | member | admin |
|---|---|---|---|---|
| Recordings | Read list / transcript | ✅ | ✅ | ✅ |
| Recordings | Ingest from Google Drive | ❌ | ✅ | ✅ |
| Recordings | Delete | ❌ | ❌ | ✅ |
| Recordings | Export signals to CRM | ❌ | ✅ | ✅ |
| Post-Call Summaries | Read | ✅ | ✅ | ✅ |
| Post-Call Summaries | Regenerate | ❌ | ✅ | ✅ |
3.5 Content Pipeline
| Resource | Action | viewer | member | admin |
|---|---|---|---|---|
| Content Pieces | Read | ✅ | ✅ | ✅ |
| Content Pieces | Create / Edit | ❌ | ✅ | ✅ |
| Content Pieces | Delete | ❌ | ❌ | ✅ |
| Content Pieces | Publish | ❌ | ✅ | ✅ |
| Attribution Data | Read | ✅ | ✅ | ✅ |
3.6 Knowledge Core
| Resource | Action | viewer | member | admin |
|---|---|---|---|---|
| Knowledge Entries | Read / Search | ✅ | ✅ | ✅ |
| Knowledge Entries | Create | ❌ | ✅ | ✅ |
| Knowledge Entries | Update / Delete | ❌ | ❌ | ✅ |
| pgvector Embeddings | Trigger re-embed | ❌ | ❌ | ✅ |
| Notion KC Sync | Trigger sync | ❌ | ❌ | ✅ |
3.7 Onboarding & Deployment Kit
| Resource | Action | viewer | member | admin |
|---|---|---|---|---|
| Deployment Kit | Read assets | ✅ | ✅ | ✅ |
| Deployment Kit | Trigger generation | ❌ | ✅ | ✅ |
| Deployment Kit | Delete / Regenerate | ❌ | ❌ | ✅ |
| Onboarding Checklist | Read | ✅ | ✅ | ✅ |
| Onboarding Checklist | Update step status | ❌ | ✅ | ✅ |
3.8 Analytics & Daily Brief
| Resource | Action | viewer | member | admin |
|---|---|---|---|---|
| Daily Brief | Read | ✅ | ✅ | ✅ |
| Daily Brief | Trigger manual generation | ❌ | ✅ | ✅ |
| Analytics Dashboard | Read | ✅ | ✅ | ✅ |
| Analytics Dashboard | Export data | ❌ | ✅ | ✅ |
3.9 Organization Administration
| Resource | Action | viewer | member | admin |
|---|---|---|---|---|
| Organization Settings | Read | ❌ | ✅ | ✅ |
| Organization Settings | Update | ❌ | ❌ | ✅ |
| Users / Invitations | Read member list | ❌ | ✅ | ✅ |
| Users / Invitations | Invite user | ❌ | ❌ | ✅ |
| Users / Invitations | Change role | ❌ | ❌ | ✅ |
| Users / Invitations | Remove user | ❌ | ❌ | ✅ |
| API Key Vault | Read key names (masked) | ❌ | ❌ | ✅ |
| API Key Vault | Create / Rotate / Delete | ❌ | ❌ | ✅ |
| Audit Log | Read | ❌ | ❌ | ✅ |
| GCS Signed URLs | Generate | ❌ | ✅ (scoped) | ✅ |
| Billing | Read / 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-007 — org:member users may not delete any resource. Deletes are exclusively org:admin actions.
BR-008 — org: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
| Variable | Purpose | Secret Store |
|---|---|---|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY | Client-side Clerk init | GCP Secret Manager → Cloud Run env |
CLERK_SECRET_KEY | Server-side Clerk API calls | GCP Secret Manager → Cloud Run env |
CLERK_WEBHOOK_SECRET | Svix webhook verification | GCP Secret Manager → Cloud Run env |
NEXT_PUBLIC_CLERK_SIGN_IN_URL | Redirect for unauthenticated users | .env / Cloud Run env |
NEXT_PUBLIC_CLERK_SIGN_UP_URL | Org creation flow entry | .env / Cloud Run env |
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL | Post-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:
- Enforce authentication on all application routes.
- Allow unauthenticated access to Clerk's own auth routes, the Svix webhook endpoint, and the Inngest serve endpoint.
- 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 Pattern | Auth Required | Org Required | Notes |
|---|---|---|---|
/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 |
/_health | ❌ | ❌ | GCP health probe |
/overview | ✅ | ✅ | GTM 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)
| Route | Method | Auth Mechanism | Notes |
|---|---|---|---|
/api/webhooks/clerk | POST | Svix HMAC (CLERK_WEBHOOK_SECRET) | BR-016, BR-017 |
/api/inngest | GET, POST | Inngest signing key | BR-018 |
Chat & Orchestrator
| Route | Method | Min. Role | BR Refs |
|---|---|---|---|
/api/chat | GET | org:viewer | BR-005 |
/api/chat | POST (stream) | org:member | BR-005, BR-006 |
/api/chat/history | GET | org:viewer | BR-005 |
/api/orchestrator/gtm | POST | org:member | BR-005, BR-006 |
/api/orchestrator/campaign | POST | org:member | BR-005, BR-006 |
Contacts
| Route | Method | Min. Role | BR Refs |
|---|---|---|---|
/api/contacts | GET | org:viewer | BR-005, BR-012 |
/api/contacts | POST | org:member | BR-005, BR-006 |
/api/contacts/[id] | GET | org:viewer | BR-005, BR-012 |
/api/contacts/[id] | PATCH | org:member | BR-005, BR-006 |
/api/contacts/[id] | DELETE | org:admin | BR-005, BR-007 |
/api/contacts/import | POST | org:member | BR-005, BR-006 |
/api/contacts/lists | GET | org:viewer | BR-005, BR-012 |
/api/contacts/lists | POST | org:member | BR-005, BR-006 |
/api/contacts/lists/[id] | PATCH | org:member | BR-005, BR-006 |
/api/contacts/lists/[id] | DELETE | org:admin | BR-005, BR-007 |
Outbound & Sequences
| Route | Method | Min. Role | BR Refs |
|---|---|---|---|
/api/sequences | GET | org:viewer | BR-005, BR-012 |
/api/sequences | POST | org:member | BR-005, BR-006 |
/api/sequences/[id] | GET | org:viewer | BR-005, BR-012 |
/api/sequences/[id] | PATCH | org:member | BR-005, BR-006 |
/api/sequences/[id] | DELETE | org:admin | BR-005, BR-007 |
/api/sequences/[id]/enroll | POST | org:member | BR-005, BR-006 |
/api/sequences/[id]/pause | POST | org:member | BR-005, BR-006 |
/api/sequences/[id]/resume | POST | org:member | BR-005, BR-006 |
/api/outbound/queue | GET | org:viewer | BR-005, BR-012 |
/api/outbound/approve | POST | org:member | BR-005, BR-019 |
/api/outbound/reject | POST | org:member | BR-005, BR-019 |
/api/outbound/hot-replies | GET | org:viewer | BR-005, BR-012 |
/api/outbound/hot-replies/[id]/respond | POST | org:member | BR-005, BR-006 |
/api/circuit-breakers | GET | org:viewer | BR-005, BR-012 |
/api/circuit-breakers/[id]/reset | POST | org:admin | BR-005, BR-009 |
/api/circuit-breakers/[id]/threshold | PATCH | org:admin | BR-005, BR-009 |
/api/mailboxes | GET | org:viewer | BR-005 |