Skip to main content

09 — Role & Permission Matrix

Ring: R1-1 (Auth & Multi-Tenant) continuation Dependency: 01-auth-multi-tenant.md (D2: Role System) Handbook: Ch. 39 (multi-tenant), Ch. 48 (security) Updated: 2026-03-27

Two-Layer Role System

Platform Roles (internal — customers DO NOT see)

RoleWhoHow AssignedScope
super_adminFounder (GaduLabs)raw_app_meta_data.platform_role = 'super_admin' (Supabase Dashboard or SQL)ALL orgs, ALL data, platform management
(future) platform_supportSupport teamSame methodRead-only access, no modifications (R3+)

Org Roles (customer-visible — separate within each org)

RoleHow AssignedSummary
ownerAutomatically assigned to org creator. Transferable.Everything + billing + org deletion
adminInvited or promoted by owner/adminMember management + all features
memberInvited (default role)All features within plan limits
viewerInvited or demotedRead-only access
The same person can have different roles in different orgs.

Page-Level Access Matrix

= full access | 👁 = view only | = no access / hidden | 🔒 = plan-dependent

Main Pages

PageRoutesuper_adminowneradminmemberviewer
Dashboard/dashboard👁
Discovery/discovery
Companies/companies👁
Company Detail/companies/[id]👁
Contacts/contacts👁
Contact Detail/contacts/[id]👁
Leads/leads👁
Lead Detail/leads/[id]👁
Scraper/scraper🔒
Products/products👁
Batch Operations/operations

Auth & Onboarding

PageRouteAccess
Login/loginPublic (if not authenticated)
Signup/signupPublic (if not authenticated)
Onboarding/onboardingNew signup, onboarding_completed = false
Auth Callback/auth/callbackOAuth redirect

Org Management

PageRoutesuper_adminowneradminmemberviewer
Org Settings/org/settings
Member Management/org/settings (tab)
Billing / Plan/org/billing
Pricing/pricing👁👁👁

Platform Admin (super_admin ONLY)

PageRoutesuper_adminAll Other Roles
Admin Dashboard/admin/dashboard
Admin Usage/admin/usage
Admin Users/admin/users
Admin Pipeline/admin/pipeline
Admin Activity/admin/activity
Admin Providers/admin/providers
Admin Segments/admin/segments
Admin Search Trends/admin/search-trends

Action-Level Permission Matrix

AI & Discovery Actions

ActionAPI Endpointsuper_adminowneradminmemberviewer
Discovery (AI search)POST /api/discover
Headhunt (contact find)POST /api/headhunt
AI Complete (general)POST /api/ai/complete
AI Classify (batch)POST /api/ai/classify
Batch ScorePOST /api/score
Discovery and headhunt consume credits — marked as costlyAction. Viewer has no permission to run AI; the button is hidden.

CRUD Actions

Actionsuper_adminowneradminmemberviewer
Create lead
Edit lead
Delete lead
Add interaction
Create task
Give feedback
Delete company
Delete contact

Scraper & Batch

Actionsuper_adminowneradminmemberviewer
Scraper upload🔒 Pro+
Scraper push🔒 Pro+
Batch Clean
Batch Discover
Batch Enrich
Batch Headhunt

Org & Member Management

Actionsuper_adminowneradminmemberviewer
Change org settings
Invite member
Remove member
Change member role✅*
Manage billing / plan
Delete org
Ownership transfer
*Admin cannot promote others to admin; can only switch between member/viewer.

Export & Data

Actionsuper_adminowneradminmemberviewer
CSV/Excel export
View search history👁

UI Visibility Rules

Hidden Elements for Viewer Role

- "Discover" button (Discovery)
- "Find Contact" button (Headhunt)
- "Save as Lead" button
- "New Interaction" / "New Task" buttons
- "Feedback" buttons
- Scraper page (hidden from sidebar)
- Batch Operations (hidden from sidebar)
- Delete buttons (lead, company, contact)
- Export buttons

Hidden Elements for Member Role

- Org Settings (hidden from sidebar)
- Billing (hidden from sidebar)
- Member management
- Batch Operations (hidden from sidebar)
- Delete buttons (company, contact — can delete their own leads)

Additional Visibility for Admin

- Org Settings access (visible in sidebar)
- Invite/remove member buttons
- Segment management (admin level)

Additional Visibility for Owner

- Billing / Plan management
- Delete org button (danger zone)
- Ownership transfer

Additional Visibility for super_admin

- /admin/* pages (separate section in sidebar)
- Cross-org navigation
- Platform metrics
- View all users/orgs

Technical Implementation

API Layer (createHandler)

// Minimum role check — added to config
export const DELETE = createHandler({
  auth: true,
  requiredRole: 'admin', // minimum admin role required
  logAction: 'lead.deleted',
}, async (request, session) => {
  // session.orgRole already available — createHandler checks it
});
requiredRole is already defined in IHandlerConfig (currently noop — to be activated).

Client Layer (UI)

// Button visibility — from useAuth hook
const { activeOrgId, orgRole } = useAuth();
// orgRole not yet available client-side — to be added (AUTH-10 extension)

// Example: hide for viewer
{orgRole !== 'viewer' && <Button>Discover</Button>}

// Example: show for admin+
{['owner', 'admin'].includes(orgRole) && <Button>Invite Member</Button>}

Middleware Layer

/admin/* → platformRole === 'super_admin' required
/org/billing → orgRole === 'owner' required
/org/settings → orgRole === 'admin' || orgRole === 'owner' required

Dynamic Permission Management (by super_admin)

This matrix is not a static document — super_admin manages it at runtime via DB. Roles are fixed (owner/admin/member/viewer) but what each role can do is configurable.

Evolution Plan

LevelWhatWhen
L1 (Beta)Roles + default permissions in DB, super_admin toggles from /admin/permissions pageR1-1 (AUTH-16)
L2 (Growth)Org owner can fine-tune within their org (grant/revoke batch permission for member)R2+
L3 (Enterprise)Custom role creation, feature-level granular permissionsR3+ (FD-3)

Data Model

-- Permission definitions (seed data — super_admin can add more)
export_ai_permission_definitions
  id UUID PK DEFAULT gen_random_uuid()
  key TEXT UNIQUE NOT NULL        -- 'page.discovery', 'action.lead.create'
  category TEXT NOT NULL          -- 'page' | 'action' | 'admin' | 'data'
  label TEXT NOT NULL             -- 'Discovery Page'
  description TEXT                -- 'Permission to run AI-powered company search'
  sort_order INT DEFAULT 0
  created_at TIMESTAMPTZ DEFAULT now()

-- Role-permission mapping (super_admin toggles)
export_ai_role_permissions
  id UUID PK DEFAULT gen_random_uuid()
  role TEXT NOT NULL CHECK(role IN ('owner','admin','member','viewer'))
  permission_key TEXT NOT NULL REFERENCES export_ai_permission_definitions(key)
  allowed BOOLEAN DEFAULT false
  updated_by UUID REFERENCES auth.users  -- who changed it
  updated_at TIMESTAMPTZ DEFAULT now()
  UNIQUE(role, permission_key)

Permission Key Convention

page.discovery          — Access to Discovery page
page.scraper            — Access to Scraper page
page.operations         — Access to Batch Operations page
page.org_settings       — Access to Org Settings page
page.org_billing        — Access to Billing page
action.discovery.run    — Run Discovery (consumes credits)
action.headhunt.run     — Run Headhunt (consumes credits)
action.lead.create      — Create lead
action.lead.delete      — Delete lead
action.company.delete   — Delete company
action.contact.delete   — Delete contact
action.scraper.upload   — Scraper upload
action.batch.run        — Start batch operation
action.export.csv       — CSV/Excel export
admin.members.invite    — Invite member
admin.members.remove    — Remove member
admin.members.role      — Change role
admin.billing.manage    — Manage plan/payment
admin.org.delete        — Delete org
admin.org.transfer      — Ownership transfer

Seed Data (default values — based on this document’s matrix)

Seed data is loaded when the app first runs or when a new permission is defined. Default values apply unless super_admin changes them.

Admin UI: /admin/permissions

┌────────────────────────────────────────────────────────────────┐
│  Permission Management                              [Save]     │
├────────────────────────────────────────────────────────────────┤
│                         owner  admin  member  viewer           │
│  ─── Pages ────────────────────────────────────────────────    │
│  Discovery               ✅     ✅      ✅      ❌            │
│  Scraper                 ✅     ✅      ☐       ❌            │
│  Batch Operations        ✅     ✅      ❌      ❌            │
│  Org Settings            ✅     ✅      ❌      ❌            │
│  Billing                 ✅     ❌      ❌      ❌            │
│  ─── Actions ──────────────────────────────────────────────    │
│  Run Discovery           ✅     ✅      ✅      ❌            │
│  Create Lead             ✅     ✅      ✅      ❌            │
│  Delete Lead             ✅     ✅      ❌      ❌            │
│  Batch Operation         ✅     ✅      ❌      ❌            │
│  CSV Export              ✅     ✅      ✅      ❌            │
│  ─── Management ───────────────────────────────────────────    │
│  Invite Member           ✅     ✅      ❌      ❌            │
│  Manage Billing          ✅     ❌      ❌      ❌            │
│  Delete Org              ✅     ❌      ❌      ❌            │
└────────────────────────────────────────────────────────────────┘
Checkbox toggle → API PATCH → DB update. Changes take effect on all active sessions immediately (on page refresh).

Implementation Flow

1. User logs in
2. useAuth → fetches role from organization_members
3. checkPermission(role, 'action.discovery.run') → DB lookup (cached)
4. UI: button visible if permitted, hidden otherwise
5. API: createHandler requiredPermission check
6. If super_admin toggles from /admin/permissions:
   → DB updated
   → Cache invalidated
   → New permission applies on next request

Cache Strategy

- Permission table is small (~80 rows: 20 permissions x 4 roles)
- Server-side: load into memory at startup, refresh with 5 min TTL
- Client-side: fetch with useAuth, keep in context
- When super_admin makes changes: cache bust endpoint (POST /api/admin/permissions/invalidate)

Future Decisions

FD-3: Enterprise Feature-Based Roles (R3+)

In the Enterprise plan, roles are not fixed owner/admin/member/viewer but feature-based permissions: can_run_discovery, can_run_batch, can_export_data, can_manage_billing At L3 level, an export_ai_custom_roles table is added for org-level custom roles.
Why not now: 4 fixed roles are sufficient. Needs will be clarified when Enterprise customers arrive.

FD-5: Role-Based Data Scope (R3+)

Some orgs may want “sales team sees only their own leads, not others’.” This is solved with data_scope: 'own' | 'team' | 'all'.
Why not now: In beta, everyone sees the same data. Data scope is an Enterprise feature.

Atomic Tasks (TODO Integration)

Tasks required to implement this matrix:
#TaskDependencyRing
AUTH-14Org settings + member invite/management pageAUTH-12R1-1
AUTH-16Dynamic permission system: DB table + seed + admin UI + checkPermissionAUTH-12R1-1
Activate requiredRole check in createHandlerAUTH-11 ✅R1-1
Add orgRole to useAuth (client-side role info)AUTH-10 ✅R1-1
Role/permission-based menu filtering in sidebarAUTH-16R1-1
Middleware /admin/* super_admin guardAUTH-12R1-1
Button/action-level permission check (UI)AUTH-16R1-1+