DataZoom

RBAC & permissions

RBAC & permissions

Last updated 5/24/2026

DataZoom — RBAC & Permissions Reference

Document ID: TECH-SEC-001
Version: 1.0
Methodology: PRD-Driven Context Engineering — v0.9 Scale
Scope: Role-Based Access Control, Clerk Integration, Protected Route Inventory, Permission Matrix


Table of Contents

  1. Executive Summary
  2. Role Hierarchy
  3. Permission Matrix
  4. Business Rules — Access Control
  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. Admin-Specific Controls
  12. Audit Logging for Permission Changes
  13. API Key Scopes
  14. Security Gaps & Recommendations

1. Executive Summary

DataZoom is a multi-tenant, AI-powered document analysis platform built on Next.js 15 (App Router) with Clerk as its authentication and multi-tenancy layer. Every user belongs to a Clerk Organization; all data is scoped to that organization. The system enforces access control at four distinct layers:

LayerMechanismLocation
Route accessClerk middleware (clerkMiddleware)product/middleware.ts
API authorizationauth() / getAuth() server-side checksEvery API route handler
Admin privilegeorg:admin role check + /admin/* route prefixproduct/app/api/admin/
Data isolationSupabase RLS policies scoped to org_idPostgreSQL

The platform does not yet implement a fine-grained custom role system beyond Clerk's built-in org:admin / org:member distinction. All member-level users share identical read/write permissions within their organization. Viewer-only access and custom roles are noted as gaps in Section 14.


2. Role Hierarchy

2.1 Clerk Organization Roles

DataZoom maps directly to Clerk's two-tier organization role model. These roles are assigned in the Clerk Dashboard and embedded in the Clerk session JWT under org_role.

┌──────────────────────────────────────────────────────────┐
│                    SYSTEM BOUNDARY                       │
│                                                          │
│  ┌─────────────────────────────────────────────────┐    │
│  │               org:admin                         │    │
│  │  • Full read/write on all resources             │    │
│  │  • Exclusive: admin pipeline controls           │    │
│  │  • Exclusive: routing statistics                │    │
│  │  • Exclusive: cap-table review approve/reject   │    │
│  │  • Exclusive: transaction void                  │    │
│  │  • Exclusive: company settings management       │    │
│  │                                                 │    │
│  │  ┌───────────────────────────────────────────┐  │    │
│  │  │             org:member                    │  │    │
│  │  │  • Read/write on documents                │  │    │
│  │  │  • Read/write on conversations            │  │    │
│  │  │  • Read/write on cap-table (data entry)   │  │    │
│  │  │  • Read/write on advisor/analysis         │  │    │
│  │  │  • Read/write on activity feed            │  │    │
│  │  │  • Read/write on collaboration sessions   │  │    │
│  │  └───────────────────────────────────────────┘  │    │
│  └─────────────────────────────────────────────────┘    │
│                                                          │
│  ✗ Unauthenticated — blocked at middleware               │
└──────────────────────────────────────────────────────────┘

2.2 Role Mapping: Clerk → DataZoom Permissions

Clerk RoleDataZoom DesignationSession Claim (org_role)Inherits From
Organization Adminadminorg:adminmember (all member permissions)
Organization Membermemberorg:member
Not authenticatedanonymousabsent— (blocked)

2.3 Special Identity: Admin Pipeline Bypass

Commit d037000 documents an explicit architectural decision: the cloud proxy endpoint (/api/admin/pipeline/cloud) uses Clerk auth directly, not the withOrgAuth wrapper. This means the admin pipeline bypass checks userId presence (any authenticated user) rather than org:admin role. This is flagged as BR-007 and a security gap in Section 14.


3. Permission Matrix

The following table covers every resource domain in DataZoom. Actions are: C = Create, R = Read, U = Update, D = Delete, X = Execute/Trigger, A = Approve/Reject.

3.1 Core Resource Permissions

ResourceActionorg:adminorg:memberanonymous
DocumentsC (upload)
DocumentsR (list/read)
DocumentsU (metadata update)
DocumentsD (delete)
Document ChunksR (semantic search)
ConversationsC
ConversationsR
ConversationsD
Timeline EventsC
Timeline EventsR
Analysis (Party/Regenerate)X
Advisor (RAG query)X
Advisor BatchX
Advisor Risk MemoX
Advisor Strategic OptionsX
Advisor Process QueueX
Activity FeedR
Activity MetricsR
Activity ExportR
Activity CalendarR
Activity RefreshX
Collaboration TokenC
Clause CompareX
Business TypesR
Business Type ProfilesR
Business Type ChecklistR
AI Interaction TrackingC

3.2 Elevated-Permission Resources (Admin Only)

ResourceActionorg:adminorg:memberanonymous
Admin PipelineR (status)
Admin PipelineX (trigger)
Admin PipelineX (retry)
Admin Pipeline CloudX (cloud dispatch)✅*
Admin Routing StatsR
Cap-Table ReviewR (queue)
Cap-Table ReviewA (approve)
Cap-Table ReviewA (reject)
Cap-Table TransactionsD (void)
Company SettingsR✅†
Company SettingsU
Company Settings LinkageR

*/api/admin/pipeline/cloud checks userId only (not org:admin) per commit d037000. See BR-007.
†Company settings GET may be readable by members depending on implementation; POST/PATCH is admin-only.

3.3 Cap-Table Feature-Gated Permissions

The cap-table module uses a feature gate (product/lib/cap-table/__tests__/feature-gate.test.ts). Access to cap-table features requires both organizational membership and the cap-table feature flag being enabled for the organization.

ResourceActionorg:admin + flagorg:member + flagNo flag
Cap-Table CurrentR
Cap-Table As-OfR
Cap-Table Auto-PopulateX
Cap-Table ExtractX
Cap-Table HealthR
Cap-Table TransactionsC/R
Cap-Table ReviewA

4. Business Rules — Access Control

Each rule below was derived from codebase patterns, commit history, and architectural documentation.


BR-001 — All application routes require authentication
Every route under the /(app) route group in product/app/(app)/ is protected by Clerk middleware. Unauthenticated requests to any /dashboard, /documents, /advisor, /cap-table, /analysis, /activity, /context, /dataroom, /admin, or /advisor path are redirected to the Clerk sign-in page. There is no anonymous or guest access to any application feature.


BR-002 — Organization membership is required to access any data
All API routes verify both userId (authenticated) and orgId (organization context) via Clerk's auth() helper. A user who is authenticated but has not joined or selected an organization will receive a 401 Unauthorized response. Data queries always include org_id as a filter, ensuring cross-organization data leakage is impossible at the application layer.


BR-003 — Admin-prefixed routes require org:admin role
All routes under /api/admin/* — specifically /api/admin/pipeline, /api/admin/pipeline/health, /api/admin/pipeline/retry, and /api/admin/routing-stats — check that the authenticated user holds the org:admin Clerk organization role. Non-admin members receive a 403 Forbidden response. The admin pipeline page at product/app/(app)/admin/pipeline/page.tsx enforces the same check at the UI layer.


BR-004 — Cap-table review and approval actions are admin-only
The review workflow endpoints (/api/cap-table/review/[id]/approve, /api/cap-table/review/[id]/reject) and the transaction void endpoint (/api/cap-table/transactions/[id]/void) require org:admin. This is enforced to ensure that equity-impacting decisions cannot be made unilaterally by non-privileged members. The review queue read endpoint (/api/cap-table/review) is also admin-only to prevent premature disclosure of pending review items. Evidence: product/lib/__tests__/review-workflow.test.ts.


BR-005 — Collaboration tokens are scoped to authenticated users within the same organization
/api/collaboration/token generates a real-time collaboration session token (used by the Tiptap collaborative editor with @tiptap/y-tiptap). The token encodes both userId and orgId. A token issued for one organization cannot be used to join a collaboration session in a different organization. This enforces document-level isolation in the real-time editing layer.


BR-006 — Company settings writes are restricted to organization admins
POST/PATCH requests to /api/company-settings and any read of /api/company-settings/linkage-mismatches require org:admin. This prevents members from altering organization-wide configuration such as business type classifications, which affect how documents are categorized and analyzed across the entire organization.


BR-007 — Cloud pipeline dispatch checks userId only (known gap)
Per commit d037000, /api/admin/pipeline/cloud was deliberately modified to use Clerk's auth() directly rather than the withOrgAuth admin wrapper. This means any authenticated user — not only org admins — can invoke cloud pipeline dispatch. This is a security regression. See Section 14 for remediation guidance.


BR-008 — Cap-table features are gated by organization-level feature flag
Access to all cap-table read and write endpoints is conditioned on the cap-table feature being enabled for the organization. The feature gate is evaluated at the API layer before any role check. An org:admin user in an organization without the cap-table feature enabled will receive a 403 with a feature-gate error code, not a role error. Evidence: product/lib/cap-table/__tests__/feature-gate.test.ts.


BR-009 — Activity export and metrics are available to all authenticated members
The activity endpoints (/api/activity/export, /api/activity/metrics, /api/activity, /api/activity/unified/*) are accessible to all org:member and org:admin users. There is no additional role restriction. Exported data is scoped to the calling user's orgId, so members can only export activity data belonging to their own organization.


BR-010 — Advisor and AI analysis endpoints are member-accessible but rate-aware
All /api/advisor/* and /api/analysis/* endpoints are accessible to any authenticated organization member. These endpoints invoke LLM services (Modal/Ollama) and consume compute credits from the organization's wallet balance. The billing/wallet system (docs/billing_wallet/) enforces spend limits per organization, which serves as a secondary control on AI endpoint consumption, even though it is not a role-based control.


BR-011 — Clerk proxy endpoint passes auth context to upstream Clerk API
/api/clerk/proxy is an authenticated proxy to the Clerk API. It requires a valid Clerk session (userId check). This endpoint should not be accessible to unauthenticated callers, as it exposes organization management operations.


BR-012 — Document folder access is scoped by organizational membership
The folder system (product/app/(app)/documents/folder/[id]/page.tsx) inherits the same authentication requirement as all app routes. Folder IDs are UUIDs stored in PostgreSQL with org_id columns; a member cannot navigate to a folder UUID belonging to a different organization because the database query will return no rows (enforced by RLS, see Section 10).


5. Clerk Integration Architecture

5.1 Organization Model

DataZoom uses Clerk's Organizations feature as its multi-tenancy primitive. Each customer (law firm, investment team, or enterprise) is a Clerk Organization. The mapping is:

Clerk Organization  ←→  DataZoom Tenant (org_id)
Clerk User          ←→  DataZoom User (user_id)
org:admin           ←→  Organization administrator
org:member          ←→  Standard user

5.2 Session Claims Structure

The Clerk JWT session token carries the following claims relevant to DataZoom's access control:

// Clerk session JWT — relevant claims
{
  "sub": "user_2abc...",           // Clerk userId
  "org_id": "org_2xyz...",         // Active organization ID
  "org_role": "org:admin",         // or "org:member"
  "org_slug": "acme-legal",        // Organization slug
  "org_permissions": [],           // Custom permissions array (currently unused)
  "metadata": {
    // publicMetadata — readable client-side
    // privateMetadata — server-side only (not exposed to client)
  }
}

The org_id claim is the primary data isolation key. Every Supabase query uses orgId extracted from auth() as a filter predicate. The org_role claim drives admin vs. member permission checks.

5.3 Custom Metadata Usage

Based on the codebase structure, custom metadata is used for:

Metadata KeyLocationPurpose
publicMetadata.onboardedclerkUser.publicMetadataTracks whether user has completed onboarding flow
publicMetadata.businessTypeclerkUser.publicMetadataStores organization's selected business type classification
privateMetadata.capTableEnabledclerkOrg.privateMetadataFeature flag controlling cap-table access (BR-008)

Note: Custom metadata is set via the Clerk Backend API (server-side only for privateMetadata) and is embedded in the session JWT on next sign-in. Changes to privateMetadata do not take effect until the user's session is refreshed.

5.4 Clerk Backend SDK Usage

Server-side auth is accessed via @clerk/nextjs/server:

// Standard pattern in API route handlers
import { auth } from '@clerk/nextjs/server';

export async function GET(request: Request) {
  const { userId, orgId, orgRole } = await auth();
  
  if (!userId || !orgId) {
    return new Response('Unauthorized', { status: 401 });
  }
  
  // Admin-only check
  if (orgRole !== 'org:admin') {
    return new Response('Forbidden', { status: 403 });
  }
  
  // ... handler logic
}

6. Middleware Configuration

6.1 File Location

product/middleware.ts

6.2 Middleware Behavior

DataZoom uses clerkMiddleware from @clerk/nextjs/server. Based on the route structure and the commit history, the middleware is configured to:

  1. Protect all /(app) routes — The entire authenticated application is behind the Clerk session gate.
  2. Allow public routes — Marketing pages (landing, pricing, etc.) under the public route group are excluded from auth requirements.
  3. Allow the Clerk webhook endpoint — If a Clerk webhook handler exists, it uses the svix-signature header for verification rather than session auth.
// product/middleware.ts — reconstructed pattern
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isPublicRoute = createRouteMatcher([
  '/',                    // Marketing homepage
  '/sign-in(.*)',        // Clerk sign-in
  '/sign-up(.*)',        // Clerk sign-up
  '/api/webhooks/(.*)', // Clerk webhook receiver
]);

export default clerkMiddleware((auth, request) => {
  if (!isPublicRoute(request)) {
    auth().protect();
  }
});

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

6.3 Admin Route Protection in Middleware

The middleware does not perform role-specific routing at the middleware layer. Admin route protection (org:admin checks) is implemented inside each API route handler individually, not as a middleware matcher rule. This means a non-admin user who navigates directly to /admin/pipeline will load the page shell before being rejected — the UI page component performs its own role check and renders an access-denied state.

Recommendation (TECH-SEC-002): Add an explicit org:admin check in clerkMiddleware for the /admin/* path prefix to fail-fast at the network edge before any React rendering occurs.


7. Protected Route Inventory

Every API route is classified by its minimum required authentication level. Routes are derived from the API routes list in the repository context.

7.1 Legend

SymbolMeaning
🔒 AUTHRequires valid Clerk session (userId + orgId)
🛡️ ADMINRequires org:admin Clerk organization role
🚩 FLAGRequires organization feature flag in addition to auth
⚠️ GAPKnown security gap (see BR-XXX)

7.2 Activity Routes

RouteFileMethod(s)Auth LevelBusiness Rule
/api/activityproduct/app/api/activity/route.tsGET, POST🔒 AUTHBR-009
/api/activity/exportproduct/app/api/activity/export/route.tsGET🔒 AUTHBR-009
/api/activity/metricsproduct/app/api/activity/metrics/route.tsGET🔒 AUTHBR-009
/api/activity/unified/calendarproduct/app/api/activity/unified/calendar/route.tsGET🔒 AUTHBR-009
/api/activity/unified/dayproduct/app/api/activity/unified/day/route.tsGET🔒 AUTHBR-009
/api/activity/unified/feedproduct/app/api/activity/unified/feed/route.tsGET🔒 AUTHBR-009
/api/activity/unified/refreshproduct/app/api/activity/unified/refresh/route.tsPOST🔒 AUTHBR-009

7.3 Admin Routes

RouteFileMethod(s)Auth LevelBusiness Rule
/api/admin/pipelineproduct/app/api/admin/pipeline/route.tsGET, POST🛡️ ADMINBR-003
/api/admin/pipeline/healthproduct/app/api/admin/pipeline/health/route.tsGET🛡️ ADMINBR-003
/api/admin/pipeline/retryproduct/app/api/admin/pipeline/retry/route.tsPOST🛡️ ADMINBR-003
/api/admin/pipeline/cloudproduct/app/api/admin/pipeline/cloud/route.tsPOST🔒 AUTH ⚠️ GAPBR-007
/api/admin/routing-statsproduct/app/api/admin/routing-stats/route.tsGET🛡️ ADMINBR-003

7.4 Advisor Routes

RouteFileMethod(s)Auth LevelBusiness Rule
/api/advisorproduct/app/api/advisor/route.tsPOST🔒 AUTHBR-010
/api/advisor/batchproduct/app/api/advisor/batch/route.tsPOST🔒 AUTHBR-010
/api/advisor/process-queueproduct/app/api/advisor/process-queue/route.tsPOST🔒 AUTHBR-010
/api/advisor/risk-memoproduct/app/api/advisor/risk-memo/route.tsPOST🔒 AUTHBR-010
/api/advisor/strategic-optionsproduct/app/api/advisor/strategic-options/route.tsPOST🔒 AUTHBR-010

7.5 AI & Analysis Routes

RouteFileMethod(s)Auth LevelBusiness Rule
/api/ai/track-interactionproduct/app/api/ai/track-interaction/route.tsPOST🔒 AUTHBR-002
/api/analysis/partyproduct/app/api/analysis/party/route.tsPOST🔒 AUTHBR-010
/api/analysis/regenerateproduct/app/api/analysis/regenerate/route.tsPOST🔒 AUTHBR-010

7.6 Business Type Routes

RouteFileMethod(s)Auth LevelBusiness Rule
/api/business-typesproduct/app/api/business-types/route.tsGET🔒 AUTHBR-002
/api/business-types/[typeKey]/checklistproduct/app/api/business-types/[typeKey]/checklist/route.tsGET🔒 AUTHBR-002
/api/business-type-profilesproduct/app/api/business-type-profiles/route.tsGET, POST🔒 AUTHBR-002
/api/business-type-profiles/[profileKey]product/app/api/business-type-profiles/[profileKey]/route.tsGET, PATCH, DELETE🔒 AUTHBR-002

7.7 Cap-Table Routes

RouteFileMethod(s)Auth LevelBusiness Rule
/api/cap-table/currentproduct/app/api/cap-table/current/route.tsGET🔒 AUTH 🚩 FLAGBR-008
/api/cap-table/as-ofproduct/app/api/cap-table/as-of/route.tsGET🔒 AUTH 🚩 FLAGBR-008
/api/cap-table/healthproduct/app/api/cap-table/health/route.tsGET🔒 AUTH 🚩 FLAGBR-008
/api/cap-table/extractproduct/app/api/cap-table/extract/route.tsPOST🔒 AUTH 🚩 FLAGBR-008
/api/cap-table/auto-populateproduct/app/api/cap-table/auto-populate/route.tsPOST🔒 AUTH 🚩 FLAGBR-008
/api/cap-table/transactionsproduct/app/api/cap-table/transactions/route.tsGET, POST🔒 AUTH 🚩 FLAGBR-008
/api/cap-table/transactions/[id]/voidproduct/app/api/cap-table/transactions/[id]/void/route.tsPOST🛡️ ADMIN 🚩 FLAGBR-004, BR-008
/api/cap-table/reviewproduct/app/api/cap-table/review/route.tsGET🛡️ ADMIN 🚩 FLAGBR-004, BR-008
/api/cap-table/review/[id]/approveproduct/app/api/cap-table/review/[id]/approve/route.tsPOST🛡️ ADMIN 🚩 FLAGBR-004, BR-008
/api/cap-table/review/[id]/rejectproduct/app/api/cap-table/review/[id]/reject/route.tsPOST🛡️ ADMIN 🚩 FLAGBR-004, BR-008

7.8 Clause Comparison Routes

RouteFileMethod(s)Auth LevelBusiness Rule
/api/clauses/compareproduct/app/api/clauses/compare/route.tsPOST🔒 AUTHBR-002
/api/clauses/compare/[id]product/app/api/clauses/compare/[id]/route.tsGET🔒 AUTHBR-002

7.9 Collaboration & Clerk Routes

RouteFileMethod(s)Auth LevelBusiness Rule
/api/collaboration/tokenproduct/app/api/collaboration/token/route.tsPOST🔒 AUTHBR-005
/api/clerk/proxyproduct/app/api/clerk/proxy/route.tsGET, POST🔒 AUTHBR-011

7.10 Company Settings Routes

RouteFileMethod(s)Auth LevelBusiness Rule
/api/company-settingsproduct/app/api/company-settings/route.tsGET🔒 AUTHBR-006
/api/company-settingsproduct/app/api/company-settings/route.tsPOST, PATCH🛡️ ADMINBR-006
/api/company-settings/linkage-mismatchesproduct/app/api/company-settings/linkage-mismatches/route.tsGET🛡️ ADMINBR-006

8. Server-Side Auth Patterns

8.1 Standard Auth Check (auth())

Used in the majority of API route handlers. Returns userId, orgId, and orgRole from the active Clerk session.

// product/app/api/advisor/route.ts — representative pattern
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const { userId, orgId, orgRole } = await auth();

  if (!userId || !orgId) {
    return NextResponse.json(
      { error: 'Unauthorized: valid session and organization required' },