Skip to main content

What can you actually do — and where are the gaps?

CAPABILITY MAP: IDENTITY & ACCESS
════════════════════════════════════════════════════════════

CAPABILITY MATURITY GAP?
────────────────────────────── ──────── ────
Authentication (Clerk) ████░ (4)
Identity Resolution (Adapter) ████░ (4)
Multi-Tenancy Schema ████░ (4)
Auto-Provisioning ███░░ (3)
Org ID Propagation ███░░ (3)
Audit Trail Schema ██░░░ (2)
Billing Schema █░░░░ (1)
Authorization (PolicyEngine) █░░░░ (1) CRITICAL
Redirect Error Handling ░░░░░ (0) CRITICAL
Role Seeding ░░░░░ (0) CRITICAL
Admin Bootstrap ░░░░░ (0) CRITICAL
Invite Flow ░░░░░ (0) GAP
Org Switching ░░░░░ (0) GAP
Security Testing ░░░░░ (0) GAP

════════════════════════════════════════════════════════════

Maturity Assessment

CapabilityMaturityEvidenceCategory
Authentication (Clerk)4 — ManagedClerk middleware validated in production, session handling working, 3-layer validationGeneric (buy)
Identity Resolution4 — ManagedClerkAuthAdapter resolves ExternalAuthId → SystemUserId → OrganisationId, branded ID typesSupporting
Multi-Tenancy Schema4 — ManagedAll business tables FK to org_organisation, composite unique constraints, typed IDsCore
Auto-Provisioning3 — DefinedCreates user + org on first login, documented pattern, but no role assignmentSupporting
Org ID Propagation3 — DefinedApplicationContext carries org ID, buildService injects context, but not enforcedCore
Audit Trail Schema2 — Repeatablegovernance_access_audit table exists in schema, not wired to any decisionSupporting
Billing Schema1 — Ad-hocorg_subscription + stripeCustomerId fields exist, nothing wiredSupporting
Authorization1 — Ad-hocPolicyEngine exists with canI() method, but defaultAllow: true bypasses everythingCore
Redirect Handling0 — Not builtNo error page — access denied causes infinite redirect loopSupporting
Role Seeding0 — Not builtgovernance_user_roles table empty, no seed scriptCore
Admin Bootstrap0 — Not builtFirst user gets no role, ADMIN_EMAILS not configuredCore
Invite Flow0 — Not builtNo invitation model, no org joining, no membership managementCore
Org Switching0 — Not builtNo UI, no resolution logic for multi-org usersSupporting
Security Testing0 — Not builtNo negative tests for cross-tenant data leakageCore

Gap Analysis

PriorityCapabilityCurrentTargetImpactFix
P1Authorization13Cannot enforce any permissionsSeed roles, switch defaultAllow to false
P1Role Seeding03No roles exist to assignMigration or seed script
P1Admin Bootstrap03Owner locked out on every deployAuto-assign Admin during provisioning
P1Redirect Handling03789 errors in 30min, unusable UXError page instead of redirect loop
P2Invite Flow03Can't onboard second user without developerInvite-by-email, membership model
P2Security Testing03No proof of data isolationNegative tests per endpoint
P3Org Switching03Multi-org users stuck in one orgOrg switcher in nav
P3Billing Schema13Can't charge per orgWire Stripe customer to org

Category Summary

CategoryCountAvg MaturityHealth
Core71.64 of 7 at zero or one — critical
Supporting61.8Authentication strong, rest weak
Generic14.0Clerk handles this well

Overall: 22% of multi-tenant checklist passing. Schema depth is real — most fails are wiring, not architecture.

Investment Strategy

TierActionCapabilityEffort
BuyAuthenticationClerk — already purchased, working well$0 (free tier)
Build (P1)Role Seeding + Admin Bootstrap + Redirect Fix + Auth enforcementCore security infrastructure2 days
Build (P2)Invite Flow + Security TestingCore user management3 days
Build (P3)Org Switching + Billing + SettingsSupporting operations5 days
DeferRLS, SSO/SAML, Parent/Child orgsEnterprise featuresFuture

Current State

What's built vs what's wired vs what's working.

LayerComponentBuiltWiredWorking
AuthenticationClerk middlewareYesYesYes
AuthenticationClerkAuthAdapter (3-layer validation)YesYesYes
IdentityAuto-provision user + "Personal Workspace" orgYesYesUntested
Identityorg_system_user table (Clerk ID → internal UUID → org)YesYesYes
IdentityBranded ID types (ExternalAuthId, SystemUserId, OrganisationId)YesYesYes
AuthorizationisOrgAdmin() guardYesYesBlocks owner
AuthorizationADMIN_EMAILS env var checkYesNot configuredNo
Authorizationgovernance_user_roles tableYesNo seed dataNo
AuthorizationPolicyEngine (canI())YesdefaultAllow: trueBypassed
Multi-tenancyorg_organisation tableYesYesYes
Multi-tenancyOrg-scoped data (all business tables FK to org)YesYesYes
Billingorg_subscription tableYesSchema onlyNo

Authentication is solid. Multi-tenancy schema is solid. Authorization is a landmine — schema exists, guards deployed, but no data and no admin bootstrapping.

Architecture Flows

Authentication (WHO are you?)

Browser → Clerk Middleware → Session Valid?
├── NO → /sign-in (public route)
└── YES → ClerkAuthAdapter.validateContext()
├── Clerk userId present?
│ ├── NO → INVALID_SESSION error
│ └── YES → Lookup in org_system_user
│ ├── FOUND → Return ApplicationContext
│ └── NOT FOUND → Auto-provision
│ ├── Create org_system_user
│ ├── Create "Personal Workspace" org
│ ├── Assign Admin role (bootstrap) ← MISSING
│ └── Return ApplicationContext
└── Error → LOOKUP_FAILED / PROVISION_FAILED

Authorization (WHAT can you do?)

Server Action called
→ buildService() → authAdapter.validateContext()
→ Returns ApplicationContext { userId, systemUserId, organizationId }
→ governance.policyEngine.canI(context, action, resourceType)
├── defaultAllow: true → ALLOW (current state — bypass)
└── defaultAllow: false → Check governance_role_permissions
├── Permission found → ALLOW
└── Permission not found → DENY → 403

Multi-Tenancy Model

Clerk User (external)
└── org_system_user (internal, branded SystemUserId)
└── org_organisation (tenant boundary)
├── All business data scoped here
├── governance_user_roles (what can this user do in this org?)
└── governance_permissions (what actions exist?)

One user can belong to multiple orgs (composite unique: external_auth_id + organisation_id)

Data Flow: Org ID Propagation

Request arrives
→ Clerk Middleware (validates session cookie)
→ ClerkAuthAdapter.validateContext()
→ Resolves: externalAuthId → systemUserId → organisationId
→ Returns ApplicationContext { systemUserId, organisationId, ... }
→ buildService(context)
→ Every repository method receives organisationId
→ Every SQL query includes WHERE organisation_id = $1
→ Response scoped to tenant

The enforcement gap: Nothing prevents a repository method from omitting the WHERE clause. Fix is either base repository class (Tier 2) or Postgres RLS (Tier 3).

Multi-Tenant Checklist

From the Multi-Tenant SaaS Checklist, 54 items audited:

SectionItemsPassPartialFail
Foundational Model5320
Data Isolation5113
User-Org Association4013
Authentication & Sessions5113
RBAC7304
Billing & Subscriptions4121
Security & Isolation5221
Total5412 (22%)10 (19%)25 (46%)

Schema depth is real — most "Fail" items have tables built but no wiring. The gap is operational, not architectural.

Gate

Before moving to Agent & Instrument Diagram:

  • All capabilities assessed with evidence-based maturity — YES (14 capabilities)
  • Capabilities categorised (Core / Supporting / Generic) — YES
  • Gaps identified and prioritised (P1 / P2 / P3) — YES (4 P1, 2 P2, 2 P3)
  • Investment strategy matches category — YES (buy generic, build core)
  • Critical gaps have escalation paths — YES (P1 gaps are Tier 0 tasks)

Context