Skip to main content

Identity & Access

Who are you, and what are you allowed to do here?


The Job

When a platform serves multiple organisations, help every user land in the right org with the right permissions — so no one is locked out, no one sees data they shouldn't, and the builder never needs to touch the database to onboard a customer.

Trigger EventCurrent FailureDesired Progress
Owner deploys production appLocked out — infinite redirect loop (PostgreSQL 22P02)First user auto-bootstrapped as Admin
Second user joinsManual DB insert or Clerk dashboard manipulationInvite-by-email → right org, right role
Second org signs upNo org isolation enforcement beyond FKAutomatic org scoping at middleware level
defaultAllow: true in productionEveryone sees everything or nothingDefault-deny, role-based access enforced

Pitch-Prompt Deck

Five cards. Five headlines. Five pictures. The meme layer80 cents in the dollar.

CardHeadlinePersuasionPictureProblem → Question → Decision
PrinciplesThe owner can't get inEthosOutcome MapAuth succeeds but authorization fails → Fix the guard or fix the query? → Fix the query — 22P02 uses wrong ID type
Performance789 errors, zero revenueLogosValue StreamAuth regression blocks all revenue PRDs → Which tier first? → Tier 0: fix query + break redirect loop
Platform80% built, 0% workingToposDependency MapSchema exists, guards deployed, no wiring → What blocks what? → User-Role query passes Clerk ID where UUID expected
ProtocolsEvery venture waits on thisKairosCapability MapKill date 27 days away, can't onboard customers → Can Tier 0 ship today? → Yes — two bugs, both have known fixes
PlayersBuilder locked from own housePathosA&IDPlatform owner can't use platform → Who fixes, who commissions? → Engineering fixes query, dream team commissions via browser

Priority Score

PRIORITY = Pain x Demand x Edge x Trend x Conversion

DimensionScore (1-5)Evidence
Pain — What's broken?5Owner locked out NOW. 789 errors in 30 minutes. Revenue kill date 2026-03-24 — can't onboard paying customers without working auth
Demand — What is needed?3Every venture (Sales CRM, Content Amplifier, Prompt Deck) requires multi-tenancy. Table stakes for SaaS. Internal demand across all products, no external demand for multi-tenancy itself
Edge — What is everyone missing?2No unique edge — multi-tenancy is commodity infrastructure. Advantage: 80% already built (schema, guards, identity layer). Speed only
Trend — Where is this heading?3Multi-tenant SaaS is stable, mature market. Not growing, not shrinking. Every B2B product requires it
Conversion — Who needs convincing?4Direct path: fix auth → unblock Sales CRM → Stripe payments → first paying customer. No conversion gap — this is prerequisite infrastructure
Composite360Platform infrastructure — enables all revenue-generating PRDs. Tiebreaker: "enables other PRDs" = builds first

The Incident

2026-02-24: Owner locked out of production dashboard. App completely inaccessible — infinite redirect loop between /dashboard and /sign-in?error=admin_access_required every ~400ms.

Root cause (from Vercel production logs, request ID dx89j-1771901662057-8dbab5d4899c):

  1. Authentication succeeds — Clerk validates session, ClerkAuthAdapter resolves userId → systemUserId → organisationId. All correct.
  2. User-Role query fails with PostgreSQL 22P02 (invalid text representation) — the role lookup passes the Clerk userId string (user_36MjIJRckjpXRqdOHaZcgMpFjzE) to a column expecting UUID. The systemUserId (f388dbc7-cb98-41ee-9708-d6f5d2c54fe0) is the correct UUID to use.
  3. Feature access denied — because the query errored (not because the role doesn't exist). The system logs feature_access_denied for dashboard read.
  4. Infinite redirect loop — dashboard denies access → redirects to /sign-in?error=admin_access_required → Clerk sees user IS authenticated → afterSignInUrl: "/dashboard" → redirect back → loop.

The bug is in the User-Role query, not the role data. Even if roles were seeded correctly, the query would still fail because it's casting a string to UUID. Fix the query first, then seed the roles.

Evidence: 789 warnings + 789 errors in the Vercel logs within 30 minutes from the redirect loop alone.

What this reveals: Authentication and authorization are different systems with different failure modes. Authentication failing = "I can't get in." Authorization failing = "I got in but I can't do anything." The second is worse — it looks like a bug, erodes trust, and blocks revenue.


Current State Audit

What's built vs what's wired.

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
Authorizationgovernance_permissions tableYesSchema onlyNo
Authorizationgovernance_role_permissions joinYesSchema onlyNo
AuthorizationPolicyEngine (canI())YesdefaultAllow: trueBypassed
Multi-tenancyorg_organisation tableYesYesYes
Multi-tenancyOrg-scoped data (all business tables FK to org)YesYesYes
Multi-tenancyOrg switching UINoNoNo
Billingorg_subscription tableYesSchema onlyNo
BillingStripe customer on orgYesNot wiredNo
SettingsOrg-level settingsPartialNoNo
SettingsUser-per-org settingsNoNoNo
AnalyticsOrg-scoped eventsNoNoNo

Summary: Authentication is solid. Multi-tenancy schema is solid. Authorization is a landmine — schema exists, guards deployed, but no data and no admin bootstrapping. Billing, settings, and analytics are unbuilt.


Multi-Tenant Checklist Audit

Every item from the Multi-Tenant SaaS Checklist, scored against the current codebase. This is the specification — every item must pass before multi-tenancy is commissioned.

1. Foundational Model

Checklist ItemStatusEvidence
Top-level tenant model exists (Organisation)Passorg_organisation table exists with branded OrganisationId type
Access model chosen and documentedPartialGitHub-style model (user belongs to multiple orgs). Decision not documented in /docs/problem-solving/decisions/tech-decisions
Users can belong to multiple orgsPassComposite unique on (external_auth_id, organisation_id) in org_system_user
Membership model existsPartialorg_system_user + agent_profile + governance_user_roles chain. Checklist asks: would a single membership table simplify?
Personal accounts are single-user orgsPassAuto-provision creates "Personal Workspace" org

2. Data Isolation

Checklist ItemStatusEvidence
organisationId on every domain tablePassAll business tables FK to org_organisation
All DB reads scoped by organisationIdUnverifiedQueries use organisation_id FK but no automated audit. No guarantee every repository method includes the filter
All DB writes include organisationIdUnverifiedSame — no enforcement layer, relies on developer discipline
Enforcement approach documentedFailLoose approach (separate FK + index) used but not documented. No compensating controls documented
Access layer enforces scopingPartialApplicationContext carries organisationId but nothing prevents a repository method from ignoring it

3. User-Org Association

Checklist ItemStatusEvidence
Invitation uses membership-first patternFailNo invitation flow exists. Users self-provision as "Personal Workspace" only
Items assigned to membership, not userPartialDeals, tasks reference agent_profile (org-scoped) but some may reference system_user directly
Revocation doesn't orphan dataFailNo revocation flow exists. No reassignment logic
Invite-before-signup worksFailNo pre-acceptance invite capability

4. Organisation ID Propagation

Checklist ItemStatusEvidence
organisationId in URLsFailURLs do not include org context. Resolved from session only. Tradeoff not documented
organisationId in all server action inputsPassVia ApplicationContext from ClerkAuthAdapter. No action trusts client-provided org ID
organisationId in all repository callsUnverifiedComposition root injects context but audit not performed
organisationId in all mutation inputsUnverifiedSame as above

5. Authentication and Sessions

Checklist ItemStatusEvidence
Session stores org contextPassApplicationContext resolves org from ClerkAuthAdapter
Concurrent org sessions supportedFailSingle active org per session. No concurrent access
Org switcher existsFailNo UI for switching orgs
Cross-org URL access worksFailNo org in URL, no resolution logic for cross-org access
Auth provider decoupledPartialClerkAuthAdapter abstracts Clerk, but no Principal type supporting multiple providers

6. RBAC

Checklist ItemStatusEvidence
Role stored per-org, not per-userPassgovernance_user_roles links agentProfileId (org-scoped) to orgRoleId
Permissions defined as code-level constantsPassgovernance_permissions table with code field
Permission check on every protected endpointFaildefaultAllow: true bypasses all checks. canI() exists but is never enforcing
Deny takes precedence over allowPassgovernance_role_permissions.effect supports allow/deny
Default-deny policyFaildefaultAllow: true — the opposite of what's required
Role visible in session without DB callFailRole requires DB query on every check
Temporal role assignmentsPasseffectiveFrom / effectiveUntil on governance_user_roles schema exists

7. Billing and Subscriptions

Checklist ItemStatusEvidence
Billing belongs to org, not userPartialSchema supports stripeCustomerId on org, but not wired
Per-seat billing counts membershipsFailNo seat counting logic
Subscription lifecycle tables existPassorg_subscription table with Stripe fields exists
Quota/usage tracking per orgPartialapi_organization_quota + api_usage_record tables exist in schema

8. Settings

Checklist ItemStatusEvidence
Org-level settingsPartialBasic fields on org_organisation (name, type). No dedicated settings table
User-per-org settingsFailNo per-org user settings on agent_profile
User global settingsFailNo global settings on user record
Settings scoped correctlyN/ANo settings exist to scope

9. Onboarding

Checklist ItemStatusEvidence
New signup creates org + membership atomicallyUnverifiedAuto-provision creates user + org but transactional guarantee not verified
Domain-based auto-joinFailNo findOrgByEmailDomain() exists
Onboarding state trackedFailNo onboarding state — half-onboarded users can access the app
Invitation acceptance links user to existing membershipFailNo invitation flow

10. Analytics

Checklist ItemStatusEvidence
Analytics events include org contextFailNo analytics events send organisationId
User linked to org in analytics providerFailNo analytics.group() call
Org-level analytics dashboardsFailNo per-org analytics

11. Security and Isolation

Checklist ItemStatusEvidence
No cross-tenant data leakage (negative tests)FailNo negative security tests exist
Fail-closed on missing org contextPartialClerkAuthAdapter validates context, but defaultAllow: true undermines it
Client-supplied IDs never trusted for ownershipPassApplicationContext resolves from session, not client input
Audit trail for access decisionsPassgovernance_access_audit table exists in schema
Soft-delete with tenant scopingPartialSoft-delete exists on some tables but not consistently applied

12. Future-Proofing

Checklist ItemStatusEvidence
Parent/child orgsFailorg_type field exists but no parent FK
Org-to-org collaborationFailNo cross-org access model
Sandbox/clone capabilityFailNo org cloning
SSO/SAML per orgFailClerk supports it, not configured

Audit Summary

SectionItemsPassPartialFailUnverified
Foundational Model53200
Data Isolation51112
User-Org Association40130
Org ID Propagation41012
Authentication & Sessions51130
RBAC73030
Billing & Subscriptions41210
Settings40121
Onboarding40031
Analytics30030
Security & Isolation52210
Future-Proofing40040
Total5412 (22%)10 (19%)25 (46%)7 (13%)

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


Demand-Side Jobs

Job 1: I Built This — Let Me In

Situation: Platform owner deploys the app. First user. No admin role exists yet. The guard that should protect the app from strangers is blocking the builder.

ElementDetail
Struggling momentDeployed production app, can't access my own dashboard
Current workaroundManually set ADMIN_EMAILS env var on Vercel, or insert role row in database
What progress looks likeFirst user (or specified bootstrap user) automatically gets admin role. No manual DB intervention required
Hidden objection"I don't want to weaken security just to fix onboarding"
Switch triggerEvery deploy where this breaks and costs debugging time

Features that serve this job:

  • Bootstrap admin: first user in an org OR ADMIN_EMAILS env var gets Admin role auto-assigned
  • Admin bootstrap runs on first login, not manual DB insert
  • ADMIN_EMAILS env var documented and configured in Vercel production
  • If isOrgAdmin() fails, show clear error message (not just a redirect param)
  • Admin status visible in user profile/settings

Job 2: Control Who Sees What

Situation: The platform grows. Sales team member joins. They should see their deals, not admin settings. A client gets a portal login. They should see their RFP status, not the pipeline.

ElementDetail
Struggling momentEveryone sees everything or everyone sees nothing — no middle ground
Current workarounddefaultAllow: true in governance config — no access control enforced
What progress looks likeThree tiers: Admin (everything), Member (own org data), Viewer (read-only portal)
Hidden objection"RBAC is complex — we'll spend weeks on permissions instead of features"
Switch triggerWhen a client sees another client's data, or a team member breaks something in admin

Features that serve this job:

  • Three roles defined: Admin, Member, Viewer
  • Role assignment on invite/onboarding
  • PolicyEngine switched from defaultAllow: true to defaultAllow: false
  • Per-resource permission checks on server actions
  • Role-based UI: admin routes, member routes, viewer routes
  • Audit trail for access decisions (governance_access_audit table already exists)

Job 3: Separate My Clients' Worlds

Situation: Platform serves multiple organisations. Each org's data must be isolated. An RFP from Company A must never appear in Company B's dashboard.

ElementDetail
Struggling momentQuerying data without org context leaks across tenants
Current workaroundAll queries scoped by organisation_id FK — but no automated enforcement
What progress looks likeOrg context injected at the middleware level. Every query filtered automatically. Impossible to forget
Hidden objection"Row-level security is hard to debug when things don't show up"
Switch triggerFirst time a user sees data that isn't theirs

Features that serve this job:

  • ApplicationContext.organizationId always present (enforced by ClerkAuthAdapter)
  • All repository queries filter by org ID (audit existing queries)
  • Database-level RLS policies as defense-in-depth (Supabase/Postgres)
  • Org switching for users who belong to multiple orgs
  • Org creation flow (name, settings, invite first members)

Job 4: Invite Someone Without a Support Ticket

Situation: Admin wants to add a team member. They shouldn't need to ask a developer to insert a database row.

ElementDetail
Struggling momentNo invite flow — users can only self-provision as Personal Workspace
Current workaroundManual database manipulation or Clerk dashboard
What progress looks likeAdmin sends email invite → recipient signs up → lands in the right org with the right role
Hidden objection"What if I invite someone by mistake? Can I revoke?"
Switch triggerWhen onboarding a second team member requires developer intervention

Features that serve this job:

  • Invite-by-email from admin settings
  • Invite creates pending membership (accepted on sign-up)
  • Role assigned at invite time
  • Invite revocable before acceptance
  • Member removable after acceptance
  • Clerk organization sync (Clerk orgs ↔ internal orgs)

Job 5: Know What's Happening in My Org

Situation: Admin needs visibility into who did what, when, in their org. Billing needs to count seats. Settings need to stay within org boundaries.

ElementDetail
Struggling momentNo org-level analytics, no billing integration, no settings scope
Current workaroundVercel logs for activity, manual Stripe for billing, no per-org settings
What progress looks likeOrg-level dashboard with activity, billing tied to org, settings scoped correctly
Hidden objection"This is plumbing — customers don't pay for settings pages"
Switch triggerFirst customer asks "how many seats am I using?" or "what did my team do this week?"

Features that serve this job:

  • Billing on org, not user — stripeCustomerId wired to org_organisation
  • Seat counting from memberships
  • Org-level settings page
  • Per-org user preferences on agent_profile
  • Analytics events include organisationId
  • Org-scoped activity feed or audit log UI

Architecture: The Desired Flow

Authentication Flow (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)
│ └── Return ApplicationContext
└── Error → LOOKUP_FAILED / PROVISION_FAILED

This flow exists and works. The gap: auto-provision creates the user and org but does NOT assign the Admin role.

Authorization Flow (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

Admin route guard (current):

/admin/layout.tsx
→ isOrgAdmin()
→ Check 1: Is email in ADMIN_EMAILS env var? → YES = admin
→ Check 2: Does user have "Admin" role in governance_user_roles? → YES = admin
→ Neither? → redirect('/dashboard?error=admin_access_required')

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. The fix is either:

  • Code-level: base repository class that requires organisationId (Tier 2)
  • Database-level: Postgres RLS policies as defense-in-depth (Tier 3)

Implementation Tiers

Tier 0: Unblock Owner (Immediate)

Fix the infinite redirect loop. Two bugs, both must be fixed.

TaskWhatWhereWhy
T0.1Fix User-Role query: use systemUserId not Clerk userIdWherever governance_user_roles is queried — the query casts a user_ prefixed string to UUID (PostgreSQL 22P02)This is the crash. Even with correct data, the query errors
T0.2Break the redirect loopDashboard and/or sign-in page — when access is denied to an authenticated user, show an error page instead of redirecting to sign-in (which redirects back)Infinite loop burns 789 errors in 30 minutes
T0.3Set ADMIN_EMAILS env var in Vercel productionVercel dashboard → Environment VariablesWorkaround: isOrgAdmin() checks this BEFORE the DB query
T0.4Seed Admin role in org_roles tableMigration or seed scriptThe role must exist before it can be assigned
T0.5Assign Admin role to owner in governance_user_rolesMigration or seed scriptOwner needs the role row
T0.6Verify: owner can access /dashboard and /admin routesBrowser commissioning protocolDream team validates via browser, not code review

Success criteria: Owner logs in → lands on dashboard → no redirect loop → can access /admin. Verified by nav (dream team) via browser commissioning.

Tier 1: Bootstrap Admin on First Login

Make Tier 0 unnecessary for future deploys.

TaskWhatWhere
T1.1During auto-provision, assign Admin role to first user in orgClerkAuthAdapter or provisioning service
T1.2During auto-provision, check ADMIN_EMAILS — if match, assign AdminSame
T1.3Show meaningful error on dashboard when admin_access_requiredDashboard page component
T1.4Add admin status indicator to user profileSettings page
T1.5Verify auto-provision is transactional (rollback on failure)Provisioning service

Success criteria: Fresh deploy → first sign-up → automatic admin → full access.

Tier 2: Role-Based Access Control

Switch from defaultAllow: true to enforced permissions.

TaskWhatWhere
T2.1Define three roles: Admin, Member, Viewerorg_roles seed data
T2.2Define permissions per rolegovernance_role_permissions seed data
T2.3Switch defaultAllow to falseComposition root config
T2.4Add canI() checks to all server actionsEach action file
T2.5Route-level guards: admin routes, member routes, viewer routesLayout components
T2.6Base repository class enforces organisationId on every queryRepository layer
T2.7Test: Member cannot access admin routesIntegration test
T2.8Test: Viewer sees read-only UIIntegration test
T2.9Test: User in org1 cannot access org2 data (negative test)Security test

Success criteria: defaultAllow: false in production. No permission = no access. Audit log captures every deny.

Tier 3: Multi-Org & Invites

Full multi-tenancy operations.

TaskWhatWhere
T3.1Invite-by-email flow (admin settings)New UI + API route
T3.2Pending invite → accepted → org membershipInvitation service
T3.3Org switcher in nav (for multi-org users)Nav component
T3.4Org creation flowSettings page
T3.5Member management (view, remove, change role)Admin settings
T3.6Sync Clerk organizations ↔ internal orgsWebhook handler
T3.7Database-level RLS as defense-in-depthPostgres policies
T3.8Items assigned to membership — audit agent_profile vs system_user referencesDomain models

Success criteria: Admin invites user by email → user signs up → lands in correct org with correct role → admin can manage members from UI.

Tier 4: Billing, Settings, Analytics

Operational infrastructure for paying customers.

TaskWhatWhere
T4.1Wire stripeCustomerId to org_organisationStripe integration
T4.2Seat counting from org membershipsBilling service
T4.3Subscription lifecycle wired (org_subscription → Stripe webhooks)Payment flow
T4.4Org-level settings pageAdmin settings UI
T4.5User-per-org settings on agent_profileSettings service
T4.6Analytics events include organisationIdEvent tracking
T4.7analytics.group() call links user to orgAnalytics provider
T4.8Org-scoped activity dashboardAdmin UI

Success criteria: Admin can see billing, manage settings, view org-scoped analytics. Stripe invoices go to the org, not the user.

Tier 5: Hardening

Defense-in-depth and future-proofing.

TaskWhatWhere
T5.1Negative security tests: cross-tenant data leakage per endpointTest suite
T5.2Fail-closed on missing org context (remove defaultAllow entirely)Middleware
T5.3Audit trail UI (read governance_access_audit)Admin UI
T5.4Soft-delete consistency auditSchema review
T5.5Document enforcement approach (loose FK vs compound PK)/docs/problem-solving/decisions/tech-decisions
T5.6Parent/child orgs (enterprise)Schema + service
T5.7SSO/SAML per org via ClerkAuth config

Success criteria: No cross-tenant leakage under any role. Security tests automated. Enforcement approach documented.


Role Definitions

RoleDashboardCRM DataRFP DataAdmin SettingsOrg ManagementInvite Users
AdminFullFull CRUDFull CRUDFullFullYes
MemberOwn orgFull CRUDFull CRUDView onlyNoNo
ViewerRead onlyRead onlyRead onlyNoNoNo

Permission Model

Permissions follow the pattern already in governance_permissions schema:

{resource_type}:{action}
Resource TypeActionsAdminMemberViewer
dashboardreadYesYesYes
contactread, create, update, deleteAllAllread
dealread, create, update, deleteAllAllread
rfp_projectread, create, update, deleteAllAllread
admin_settingsread, updateAllreadNo
org_membersread, invite, remove, change_roleAllNoNo
org_settingsread, updateAllNoNo
billingread, updateAllNoNo
analyticsreadAllreadNo

Commissioning

Browser-based validation against this PRD. The builder never validates their own work.

ComponentTierStatusHow to Verify
Owner can access /dashboardT0BlockedNavigate to dreamineering.com/dashboard, confirm no redirect loop
Owner can access /adminT0BlockedNavigate to dreamineering.com/admin, confirm admin panel renders
ADMIN_EMAILS env var configuredT0FailCheck Vercel dashboard → Environment Variables
Roles seeded in databaseT0FailAdmin panel → user management (or direct DB query)
Auto-bootstrap Admin on first loginT1Not startedFresh deploy, new user sign-up, verify Admin role assigned
Error page instead of redirect loopT1Not startedUnauthenticated access → clear error message visible
Three roles defined (Admin, Member, Viewer)T2Not startedAdmin panel → roles management
defaultAllow: false enforcedT2Not startedViewer role cannot access admin routes
Cross-tenant isolation (negative test)T2Not startedLog in as user in org1, attempt to access org2 data via URL manipulation
Invite-by-email works end-to-endT3Not startedAdmin sends invite → recipient signs up → lands in correct org
Org switcher functionalT3Not startedUser with 2+ orgs can switch between them
Stripe billing on orgT4Not startedPayment creates invoice tied to org, not user
Org-scoped analytics visibleT4Not startedAdmin sees activity scoped to their org only
No cross-tenant leakage (security test suite)T5Not startedAutomated test suite passes for all endpoints

Risk + Kill Signal

RiskLikelihoodImpactMitigation
Auth regression blocks every deployHHTier 0 is immediate. Tier 1 prevents recurrence
defaultAllow: true leaks data before Tier 2MHAll data already scoped by org FK. defaultAllow affects route guards, not data queries
RBAC complexity delays revenue-generating featuresMMThree roles only. No custom permissions until proven needed
Clerk lock-in prevents auth provider switchLMClerkAuthAdapter abstracts Clerk. Adding a new provider = new adapter, same interface
Cross-tenant leakage discovered by customerLHNegative security tests in Tier 2. RLS in Tier 3. Defense-in-depth

Kill signal: None — this is mandatory infrastructure. Every SaaS product requires multi-tenancy. The question is not "should we build this" but "in what order." Kill signals apply to the ventures that depend on this, not to this capability itself.


Business Validation

QuestionAnswer
Problem exists?Yes — owner locked out of own production app right now
People pay for this?Table stakes. Every SaaS needs auth/authz. Not a differentiator — a requirement
Can we deliver?80% built. Schema exists. Guards exist. Gap is wiring + seed data + bootstrap logic
Unit economics?Clerk free tier covers initial usage. No additional infrastructure cost
Kill signal?None — this is mandatory infrastructure. Ship or don't ship the platform

Success Criteria

CriteriaMetricTargetHow to Measure
Owner accessLogin → dashboard without redirectZero redirect loopsBrowser commissioning
Admin bootstrapFirst user in new org gets Admin role100% of new orgsAuto-provision test
ADMIN_EMAILSEnv var set and documentedConfigured in VercelVercel dashboard check
Error messagingHuman-readable error on access deniedNo raw query paramsBrowser commissioning
Role seedingThree roles in databaseAdmin, Member, ViewerDB query or admin panel
Default-denydefaultAllow: false in productionNo bypassed permissionsConfig review + integration test
Invite flowEmail invite → sign-up → correct org + roleEnd-to-end worksBrowser commissioning
Cross-tenant isolationUser in org1 cannot access org2 dataZero leakageNegative security test
Billing on orgStripe customer tied to orgInvoice shows org nameStripe dashboard
Checklist complianceMulti-Tenant SaaS Checklist items passing80%+ Pass (currently 22%)Re-audit after each tier

Context