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
- Executive Summary
- Role Hierarchy
- Permission Matrix
- Business Rules — Access Control
- Clerk Integration Architecture
- Middleware Configuration
- Protected Route Inventory
- Server-Side Auth Patterns
- Client-Side Auth Guards
- Row-Level Security
- Admin-Specific Controls
- Audit Logging for Permission Changes
- API Key Scopes
- 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:
| Layer | Mechanism | Location |
|---|---|---|
| Route access | Clerk middleware (clerkMiddleware) | product/middleware.ts |
| API authorization | auth() / getAuth() server-side checks | Every API route handler |
| Admin privilege | org:admin role check + /admin/* route prefix | product/app/api/admin/ |
| Data isolation | Supabase RLS policies scoped to org_id | PostgreSQL |
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 Role | DataZoom Designation | Session Claim (org_role) | Inherits From |
|---|---|---|---|
| Organization Admin | admin | org:admin | member (all member permissions) |
| Organization Member | member | org:member | — |
| Not authenticated | anonymous | absent | — (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
| Resource | Action | org:admin | org:member | anonymous |
|---|---|---|---|---|
| Documents | C (upload) | ✅ | ✅ | ❌ |
| Documents | R (list/read) | ✅ | ✅ | ❌ |
| Documents | U (metadata update) | ✅ | ✅ | ❌ |
| Documents | D (delete) | ✅ | ✅ | ❌ |
| Document Chunks | R (semantic search) | ✅ | ✅ | ❌ |
| Conversations | C | ✅ | ✅ | ❌ |
| Conversations | R | ✅ | ✅ | ❌ |
| Conversations | D | ✅ | ✅ | ❌ |
| Timeline Events | C | ✅ | ✅ | ❌ |
| Timeline Events | R | ✅ | ✅ | ❌ |
| Analysis (Party/Regenerate) | X | ✅ | ✅ | ❌ |
| Advisor (RAG query) | X | ✅ | ✅ | ❌ |
| Advisor Batch | X | ✅ | ✅ | ❌ |
| Advisor Risk Memo | X | ✅ | ✅ | ❌ |
| Advisor Strategic Options | X | ✅ | ✅ | ❌ |
| Advisor Process Queue | X | ✅ | ✅ | ❌ |
| Activity Feed | R | ✅ | ✅ | ❌ |
| Activity Metrics | R | ✅ | ✅ | ❌ |
| Activity Export | R | ✅ | ✅ | ❌ |
| Activity Calendar | R | ✅ | ✅ | ❌ |
| Activity Refresh | X | ✅ | ✅ | ❌ |
| Collaboration Token | C | ✅ | ✅ | ❌ |
| Clause Compare | X | ✅ | ✅ | ❌ |
| Business Types | R | ✅ | ✅ | ❌ |
| Business Type Profiles | R | ✅ | ✅ | ❌ |
| Business Type Checklist | R | ✅ | ✅ | ❌ |
| AI Interaction Tracking | C | ✅ | ✅ | ❌ |
3.2 Elevated-Permission Resources (Admin Only)
| Resource | Action | org:admin | org:member | anonymous |
|---|---|---|---|---|
| Admin Pipeline | R (status) | ✅ | ❌ | ❌ |
| Admin Pipeline | X (trigger) | ✅ | ❌ | ❌ |
| Admin Pipeline | X (retry) | ✅ | ❌ | ❌ |
| Admin Pipeline Cloud | X (cloud dispatch) | ✅* | ❌ | ❌ |
| Admin Routing Stats | R | ✅ | ❌ | ❌ |
| Cap-Table Review | R (queue) | ✅ | ❌ | ❌ |
| Cap-Table Review | A (approve) | ✅ | ❌ | ❌ |
| Cap-Table Review | A (reject) | ✅ | ❌ | ❌ |
| Cap-Table Transactions | D (void) | ✅ | ❌ | ❌ |
| Company Settings | R | ✅ | ✅† | ❌ |
| Company Settings | U | ✅ | ❌ | ❌ |
| Company Settings Linkage | R | ✅ | ❌ | ❌ |
*
/api/admin/pipeline/cloudchecksuserIdonly (notorg:admin) per commitd037000. See BR-007.
†Company settingsGETmay be readable by members depending on implementation;POST/PATCHis 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.
| Resource | Action | org:admin + flag | org:member + flag | No flag |
|---|---|---|---|---|
| Cap-Table Current | R | ✅ | ✅ | ❌ |
| Cap-Table As-Of | R | ✅ | ✅ | ❌ |
| Cap-Table Auto-Populate | X | ✅ | ✅ | ❌ |
| Cap-Table Extract | X | ✅ | ✅ | ❌ |
| Cap-Table Health | R | ✅ | ✅ | ❌ |
| Cap-Table Transactions | C/R | ✅ | ✅ | ❌ |
| Cap-Table Review | A | ✅ | ❌ | ❌ |
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 Key | Location | Purpose |
|---|---|---|
publicMetadata.onboarded | clerkUser.publicMetadata | Tracks whether user has completed onboarding flow |
publicMetadata.businessType | clerkUser.publicMetadata | Stores organization's selected business type classification |
privateMetadata.capTableEnabled | clerkOrg.privateMetadata | Feature 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 toprivateMetadatado 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:
- Protect all
/(app)routes — The entire authenticated application is behind the Clerk session gate. - Allow public routes — Marketing pages (landing, pricing, etc.) under the public route group are excluded from auth requirements.
- Allow the Clerk webhook endpoint — If a Clerk webhook handler exists, it uses the
svix-signatureheader 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
| Symbol | Meaning |
|---|---|
🔒 AUTH | Requires valid Clerk session (userId + orgId) |
🛡️ ADMIN | Requires org:admin Clerk organization role |
🚩 FLAG | Requires organization feature flag in addition to auth |
⚠️ GAP | Known security gap (see BR-XXX) |
7.2 Activity Routes
| Route | File | Method(s) | Auth Level | Business Rule |
|---|---|---|---|---|
/api/activity | product/app/api/activity/route.ts | GET, POST | 🔒 AUTH | BR-009 |
/api/activity/export | product/app/api/activity/export/route.ts | GET | 🔒 AUTH | BR-009 |
/api/activity/metrics | product/app/api/activity/metrics/route.ts | GET | 🔒 AUTH | BR-009 |
/api/activity/unified/calendar | product/app/api/activity/unified/calendar/route.ts | GET | 🔒 AUTH | BR-009 |
/api/activity/unified/day | product/app/api/activity/unified/day/route.ts | GET | 🔒 AUTH | BR-009 |
/api/activity/unified/feed | product/app/api/activity/unified/feed/route.ts | GET | 🔒 AUTH | BR-009 |
/api/activity/unified/refresh | product/app/api/activity/unified/refresh/route.ts | POST | 🔒 AUTH | BR-009 |
7.3 Admin Routes
| Route | File | Method(s) | Auth Level | Business Rule |
|---|---|---|---|---|
/api/admin/pipeline | product/app/api/admin/pipeline/route.ts | GET, POST | 🛡️ ADMIN | BR-003 |
/api/admin/pipeline/health | product/app/api/admin/pipeline/health/route.ts | GET | 🛡️ ADMIN | BR-003 |
/api/admin/pipeline/retry | product/app/api/admin/pipeline/retry/route.ts | POST | 🛡️ ADMIN | BR-003 |
/api/admin/pipeline/cloud | product/app/api/admin/pipeline/cloud/route.ts | POST | 🔒 AUTH ⚠️ GAP | BR-007 |
/api/admin/routing-stats | product/app/api/admin/routing-stats/route.ts | GET | 🛡️ ADMIN | BR-003 |
7.4 Advisor Routes
| Route | File | Method(s) | Auth Level | Business Rule |
|---|---|---|---|---|
/api/advisor | product/app/api/advisor/route.ts | POST | 🔒 AUTH | BR-010 |
/api/advisor/batch | product/app/api/advisor/batch/route.ts | POST | 🔒 AUTH | BR-010 |
/api/advisor/process-queue | product/app/api/advisor/process-queue/route.ts | POST | 🔒 AUTH | BR-010 |
/api/advisor/risk-memo | product/app/api/advisor/risk-memo/route.ts | POST | 🔒 AUTH | BR-010 |
/api/advisor/strategic-options | product/app/api/advisor/strategic-options/route.ts | POST | 🔒 AUTH | BR-010 |
7.5 AI & Analysis Routes
| Route | File | Method(s) | Auth Level | Business Rule |
|---|---|---|---|---|
/api/ai/track-interaction | product/app/api/ai/track-interaction/route.ts | POST | 🔒 AUTH | BR-002 |
/api/analysis/party | product/app/api/analysis/party/route.ts | POST | 🔒 AUTH | BR-010 |
/api/analysis/regenerate | product/app/api/analysis/regenerate/route.ts | POST | 🔒 AUTH | BR-010 |
7.6 Business Type Routes
| Route | File | Method(s) | Auth Level | Business Rule |
|---|---|---|---|---|
/api/business-types | product/app/api/business-types/route.ts | GET | 🔒 AUTH | BR-002 |
/api/business-types/[typeKey]/checklist | product/app/api/business-types/[typeKey]/checklist/route.ts | GET | 🔒 AUTH | BR-002 |
/api/business-type-profiles | product/app/api/business-type-profiles/route.ts | GET, POST | 🔒 AUTH | BR-002 |
/api/business-type-profiles/[profileKey] | product/app/api/business-type-profiles/[profileKey]/route.ts | GET, PATCH, DELETE | 🔒 AUTH | BR-002 |
7.7 Cap-Table Routes
| Route | File | Method(s) | Auth Level | Business Rule |
|---|---|---|---|---|
/api/cap-table/current | product/app/api/cap-table/current/route.ts | GET | 🔒 AUTH 🚩 FLAG | BR-008 |
/api/cap-table/as-of | product/app/api/cap-table/as-of/route.ts | GET | 🔒 AUTH 🚩 FLAG | BR-008 |
/api/cap-table/health | product/app/api/cap-table/health/route.ts | GET | 🔒 AUTH 🚩 FLAG | BR-008 |
/api/cap-table/extract | product/app/api/cap-table/extract/route.ts | POST | 🔒 AUTH 🚩 FLAG | BR-008 |
/api/cap-table/auto-populate | product/app/api/cap-table/auto-populate/route.ts | POST | 🔒 AUTH 🚩 FLAG | BR-008 |
/api/cap-table/transactions | product/app/api/cap-table/transactions/route.ts | GET, POST | 🔒 AUTH 🚩 FLAG | BR-008 |
/api/cap-table/transactions/[id]/void | product/app/api/cap-table/transactions/[id]/void/route.ts | POST | 🛡️ ADMIN 🚩 FLAG | BR-004, BR-008 |
/api/cap-table/review | product/app/api/cap-table/review/route.ts | GET | 🛡️ ADMIN 🚩 FLAG | BR-004, BR-008 |
/api/cap-table/review/[id]/approve | product/app/api/cap-table/review/[id]/approve/route.ts | POST | 🛡️ ADMIN 🚩 FLAG | BR-004, BR-008 |
/api/cap-table/review/[id]/reject | product/app/api/cap-table/review/[id]/reject/route.ts | POST | 🛡️ ADMIN 🚩 FLAG | BR-004, BR-008 |
7.8 Clause Comparison Routes
| Route | File | Method(s) | Auth Level | Business Rule |
|---|---|---|---|---|
/api/clauses/compare | product/app/api/clauses/compare/route.ts | POST | 🔒 AUTH | BR-002 |
/api/clauses/compare/[id] | product/app/api/clauses/compare/[id]/route.ts | GET | 🔒 AUTH | BR-002 |
7.9 Collaboration & Clerk Routes
| Route | File | Method(s) | Auth Level | Business Rule |
|---|---|---|---|---|
/api/collaboration/token | product/app/api/collaboration/token/route.ts | POST | 🔒 AUTH | BR-005 |
/api/clerk/proxy | product/app/api/clerk/proxy/route.ts | GET, POST | 🔒 AUTH | BR-011 |
7.10 Company Settings Routes
| Route | File | Method(s) | Auth Level | Business Rule |
|---|---|---|---|---|
/api/company-settings | product/app/api/company-settings/route.ts | GET | 🔒 AUTH | BR-006 |
/api/company-settings | product/app/api/company-settings/route.ts | POST, PATCH | 🛡️ ADMIN | BR-006 |
/api/company-settings/linkage-mismatches | product/app/api/company-settings/linkage-mismatches/route.ts | GET | 🛡️ ADMIN | BR-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' },