Skip to content

Phase 2: Production RBAC Security Rules - Architectural Design

Document Type: Architecture / Thinking
Status: Conceptual Design
Created: 2026-01-15
Target Implementation: When roles are finalized (Wave 5+)
Dependencies: Custom Claims Infrastructure, AuthzSnapshot System, Directory Graph


Executive Summary

This document defines the architectural approach for transitioning from Development Mode (authenticated-only) security rules to Production RBAC (role-based access control) security rules. The design integrates the platform's existing 4-Axis Authorization Model with Firebase Firestore security rules to create a comprehensive, relationship-backed security system.

Key Principles

  1. Relationship-Backed: Roles derived from Directory graph traversal, not hardcoded claims
  2. Context-Aware: Access determined by Hat (persona), Scope (boundary), Asset (target), and Principal (delegation)
  3. Snapshot-Driven: Custom claims store pre-computed AuthzSnapshot for performance
  4. Forensically Verified: Capabilities only granted after verification events (Standard 78)
  5. Delegation-Ready: First-class support for proxy voting and property management

1. Authorization Model Integration

1.1 The 4-Axis Context Model

The platform's authorization is governed by four axes (from 02_B_AUTHZ_MODEL.md):

Axis Field Firebase Mapping Security Rule Usage
Hat activeHat request.auth.token.activeHat Primary role check
Scope activeScope request.auth.token.activeScope Boundary enforcement
Asset activeAssetId request.auth.token.activeAssetId Ownership verification
Principal actingForPrincipalId request.auth.token.actingForPrincipalId Delegation check

1.2 Role Hierarchy (Persona Book)

Primary Personas (Hats): - PUBLIC (0) - Unauthenticated or unverified - GUEST (10) - Short-term visitor (reservation-bound) - TENANT (20) - Leaseholder - RESIDENT (30) - Family/cohabitant - OWNER (40) - Primary title holder - CO_OWNER (40) - Secondary title holder - AGENT (50) - Property manager, sales agent - VENDOR (60) - Service provider - LEGAL (70) - Legal counsel, notary - GOVERNANCE (80) - Board member, vigilance - STAFF (90) - Building staff (with sub-facets) - ADMIN (100) - System administrator - GOD (1000) - Superuser/root

Staff Sub-Facets: - STAFF_MAINTENANCE - Maintenance department - STAFF_SECURITY - Security personnel - STAFF_FRONTDESK - Front desk/hospitality - STAFF_ENGINEERING - Building engineer - STAFF_ACCOUNTING - Accounting department - STAFF_CLEANING - Cleaning services

Governance Sub-Facets: - GOVERNANCE_PRESIDENT - Board president - GOVERNANCE_TREASURER - Board treasurer - GOVERNANCE_SECRETARY - Board secretary - GOVERNANCE_VIGILANCE - Vigilance committee member

1.3 Scope Boundaries

  • my_units - User can only access their owned/managed units
  • building - User can access building-wide data
  • community - User can access community-level data (future)

2. Custom Claims Architecture

2.1 Claims Structure

Firebase Auth custom claims will store a snapshot of the user's authorization state:

interface FirebaseCustomClaims {
  // Primary Context
  activeHat: UserRole;              // Current acting persona
  activeScope: 'my_units' | 'building';
  activeAssetId?: string;           // Current target unit/asset
  actingForPrincipalId?: string;    // Delegation target

  // Authorization Snapshot
  authzVersion: number;             // Dirty detection timestamp
  availableHats: UserRole[];        // All personas user can assume
  availableAssets: string[];        // All units user can access
  capabilities: string[];           // Pre-computed capability IDs

  // Metadata
  directoryProfileId: string;       // Link to Directory
  isDomainUser: boolean;            // @singulardream.org email
  lastRoleCheck: number;            // Timestamp of last graph traversal
}

2.2 Claims Lifecycle

1. Initial Authentication (Login)

// Server Action: apps/platform/src/actions/auth-context.ts
async function onUserLogin(firebaseUser: FirebaseUser) {
  // 1. Resolve Directory Profile
  const profile = await getDirectoryProfile(firebaseUser.email);

  // 2. Traverse Relationship Graph
  const snapshot = await resolveAuthzSnapshot(profile);

  // 3. Set Custom Claims
  await admin.auth().setCustomUserClaims(firebaseUser.uid, {
    activeHat: snapshot.availableHats[0],
    activeScope: deriveDefaultScope(snapshot.availableHats[0]),
    authzVersion: snapshot.authzVersion,
    availableHats: snapshot.availableHats,
    availableAssets: snapshot.availableAssets.map(a => a.id),
    capabilities: Object.keys(snapshot.capabilities).filter(k => snapshot.capabilities[k]),
    directoryProfileId: profile.id,
    isDomainUser: firebaseUser.email.endsWith('@singulardream.org'),
    lastRoleCheck: Date.now()
  });

  // 4. Force token refresh
  await firebaseUser.getIdToken(true);
}

2. Context Switch (Hat Change)

// Server Action: apps/platform/src/actions/context-actions.ts
async function switchHat(newHat: UserRole) {
  const user = await getCurrentUser();
  const claims = await user.getIdTokenResult();

  // Verify user has access to this hat
  if (!claims.claims.availableHats.includes(newHat)) {
    throw new Error('Unauthorized hat');
  }

  // Update claims
  await admin.auth().setCustomUserClaims(user.uid, {
    ...claims.claims,
    activeHat: newHat,
    activeScope: deriveDefaultScope(newHat),
    activeAssetId: null // Reset asset on hat change
  });

  // Force token refresh
  await user.getIdToken(true);
}

3. Dirty Detection (Relationship Change)

// Cloud Function: functions/src/onDirectoryChange.ts
export const onDirectoryChange = functions.firestore
  .document('directory_profiles/{profileId}')
  .onUpdate(async (change, context) => {
    const before = change.before.data();
    const after = change.after.data();

    // Check if relationships changed
    if (JSON.stringify(before.relationships) !== JSON.stringify(after.relationships)) {
      // Bump authzVersion
      await change.after.ref.update({
        authzVersion: admin.firestore.FieldValue.serverTimestamp()
      });

      // Find associated Firebase user
      const userRecord = await admin.auth().getUserByEmail(after.email);

      // Re-resolve snapshot
      const snapshot = await resolveAuthzSnapshot(after);

      // Update claims
      await admin.auth().setCustomUserClaims(userRecord.uid, {
        ...userRecord.customClaims,
        authzVersion: snapshot.authzVersion,
        availableHats: snapshot.availableHats,
        availableAssets: snapshot.availableAssets.map(a => a.id),
        capabilities: Object.keys(snapshot.capabilities).filter(k => snapshot.capabilities[k])
      });
    }
  });


3. Firestore Security Rules Design

3.1 Helper Functions

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {

    // ============================================================================
    // HELPER FUNCTIONS
    // ============================================================================

    // --- Authentication ---

    function isAuthenticated() {
      return request.auth != null;
    }

    function isOwner(userId) {
      return request.auth.uid == userId;
    }

    function isDomainUser() {
      return isAuthenticated() && 
             request.auth.token.isDomainUser == true;
    }

    // --- Role Checks ---

    function hasHat(hat) {
      return isAuthenticated() && 
             request.auth.token.activeHat == hat;
    }

    function hasAnyHat(hats) {
      return isAuthenticated() && 
             request.auth.token.activeHat in hats;
    }

    function canAssumeHat(hat) {
      return isAuthenticated() && 
             request.auth.token.availableHats != null &&
             hat in request.auth.token.availableHats;
    }

    // --- Capability Checks ---

    function hasCapability(capabilityId) {
      return isAuthenticated() && 
             request.auth.token.capabilities != null &&
             capabilityId in request.auth.token.capabilities;
    }

    // --- Scope Checks ---

    function hasScope(scope) {
      return isAuthenticated() && 
             request.auth.token.activeScope == scope;
    }

    function isBuildingScope() {
      return hasScope('building');
    }

    function isMyUnitsScope() {
      return hasScope('my_units');
    }

    // --- Asset Ownership ---

    function ownsAsset(assetId) {
      return isAuthenticated() && 
             request.auth.token.availableAssets != null &&
             assetId in request.auth.token.availableAssets;
    }

    function isActiveAsset(assetId) {
      return isAuthenticated() && 
             request.auth.token.activeAssetId == assetId;
    }

    // --- Delegation ---

    function isActingFor(principalId) {
      return isAuthenticated() && 
             request.auth.token.actingForPrincipalId == principalId;
    }

    function isDelegated() {
      return isAuthenticated() && 
             request.auth.token.actingForPrincipalId != null;
    }

    // --- Privilege Levels ---

    function isPrivileged() {
      return hasAnyHat(['ADMIN', 'GOD', 'STAFF', 'GOVERNANCE']);
    }

    function isAdmin() {
      return hasAnyHat(['ADMIN', 'GOD']);
    }

    function isStaff() {
      return hasAnyHat([
        'STAFF', 
        'STAFF_MAINTENANCE', 
        'STAFF_SECURITY', 
        'STAFF_FRONTDESK', 
        'STAFF_ENGINEERING',
        'STAFF_ACCOUNTING',
        'STAFF_CLEANING'
      ]);
    }

    function isGovernance() {
      return hasAnyHat([
        'GOVERNANCE',
        'GOVERNANCE_PRESIDENT',
        'GOVERNANCE_TREASURER',
        'GOVERNANCE_SECRETARY',
        'GOVERNANCE_VIGILANCE'
      ]);
    }

    function isOwnerOrResident() {
      return hasAnyHat(['OWNER', 'CO_OWNER', 'RESIDENT']);
    }

3.2 Collection-Specific Rules

Finance Collections

    // --- Finance: Accounts ---
    match /finance_accounts/{accountId} {
      allow read: if isAdmin() || 
                     isStaff() || 
                     isGovernance();
      allow write: if isAdmin() || 
                      (isStaff() && hasCapability('FIN-MANAGE-COA'));
    }

    // --- Finance: Ledger ---
    match /finance_ledger/{entryId} {
      allow read: if isAdmin() || 
                     isStaff() || 
                     isGovernance();
      allow create: if isAdmin() || 
                       (isStaff() && hasCapability('FIN-POST-JOURNAL'));
      allow update, delete: if isAdmin();
    }

    // --- Finance: Bank Statements ---
    match /finance_bank_statements/{statementId} {
      allow read: if isAdmin() || 
                     (isStaff() && hasCapability('FIN-VIEW-STATEMENTS')) ||
                     (isGovernance() && hasCapability('GOV-AUDIT-FINANCE'));
      allow write: if isAdmin() || 
                      (isStaff() && hasCapability('FIN-MANAGE-STATEMENTS'));
    }

    // --- Finance: Invoices ---
    match /finance_invoices/{invoiceId} {
      allow read: if isAdmin() || 
                     isStaff() || 
                     isGovernance() ||
                     (isOwnerOrResident() && ownsAsset(resource.data.assetId));
      allow write: if isAdmin() || 
                      (isStaff() && hasCapability('FIN-MANAGE-INVOICES'));
    }

    // --- Finance: Assessments ---
    match /finance_assessments/{assessmentId} {
      allow read: if isAuthenticated(); // All residents can see assessments
      allow write: if isAdmin() || 
                      (isGovernance() && hasCapability('GOV-APPROVE-BUDGET'));
    }

Operations Collections

    // --- Operations: Personnel ---
    match /operations_personnel/{personnelId} {
      allow read: if isAdmin() || isStaff();
      allow write: if isAdmin() || 
                      (isStaff() && hasCapability('OPS-MANAGE-PERSONNEL'));
    }

    // --- Operations: Work Orders ---
    match /operations_work_orders/{orderId} {
      allow read: if isAdmin() || 
                     isStaff() ||
                     (isOwnerOrResident() && ownsAsset(resource.data.assetId));
      allow create: if isAuthenticated(); // Anyone can create work orders
      allow update: if isAdmin() || 
                       (isStaff() && hasCapability('OPS-MANAGE-WORK-ORDERS')) ||
                       (isOwner(resource.data.createdBy) && resource.data.status == 'draft');
      allow delete: if isAdmin();
    }

    // --- Operations: Amenity Bookings ---
    match /operations_bookings/{bookingId} {
      allow read: if isAdmin() || 
                     isStaff() ||
                     isOwner(resource.data.userId);
      allow create: if isOwnerOrResident() && 
                       hasCapability('PROP-BOOK-AMENITY');
      allow update: if isAdmin() || 
                       isStaff() ||
                       (isOwner(resource.data.userId) && resource.data.status == 'pending');
      allow delete: if isAdmin() || 
                       (isOwner(resource.data.userId) && resource.data.status == 'pending');
    }

    // --- Operations: Access Logs ---
    match /operations_access_logs/{logId} {
      allow read: if isAdmin() || 
                     (isStaff() && hasAnyHat(['STAFF_SECURITY', 'STAFF_FRONTDESK']));
      allow create: if isStaff(); // Security system creates logs
      allow update, delete: if false; // Immutable
    }

Governance Collections

    // --- Governance: Assemblies ---
    match /governance_assemblies/{assemblyId} {
      allow read: if isOwnerOrResident() || isGovernance() || isAdmin();
      allow create: if isGovernance() && hasCapability('GOV-CREATE-ASSEMBLY');
      allow update: if isGovernance() && hasCapability('GOV-MANAGE-ASSEMBLY');
      allow delete: if isAdmin();
    }

    // --- Governance: Proposals ---
    match /governance_proposals/{proposalId} {
      allow read: if isOwnerOrResident() || isGovernance() || isAdmin();
      allow create: if (isOwnerOrResident() && hasCapability('GOV-CREATE-PROPOSAL')) ||
                       (isGovernance() && hasCapability('GOV-CREATE-PROPOSAL'));
      allow update: if isGovernance() || 
                       (isOwner(resource.data.createdBy) && resource.data.status == 'draft');
      allow delete: if isAdmin();
    }

    // --- Governance: Votes ---
    match /governance_votes/{voteId} {
      allow read: if isGovernance() || isAdmin();
      allow create: if isAuthenticated() && 
                       hasCapability('GOV-VOTE') &&
                       ownsAsset(request.resource.data.assetId);
      allow update, delete: if false; // Votes are immutable
    }

    // --- Governance: Proxies ---
    match /governance_proxies/{proxyId} {
      allow read: if isOwner(resource.data.ownerId) || 
                     isOwner(resource.data.proxyId) ||
                     isGovernance() ||
                     isAdmin();
      allow create: if isAuthenticated() && 
                       isOwner(request.resource.data.ownerId) &&
                       hasCapability('GOV-DELEGATE-VOTE');
      allow update: if isOwner(resource.data.ownerId);
      allow delete: if isOwner(resource.data.ownerId) || isAdmin();
    }

Knowledge Collections

    // --- Knowledge: Articles ---
    match /knowledge_articles/{articleId} {
      allow read: if resource.data.status == 'published' ||
                     isStaff() ||
                     isGovernance() ||
                     isAdmin();
      allow write: if isAdmin() || 
                      (isStaff() && hasCapability('KNOW-MANAGE-ARTICLES'));
    }

    // --- Knowledge: Vault (Restricted Documents) ---
    match /knowledge_vault/{documentId} {
      // Role-based access using audience field
      allow read: if isAdmin() ||
                     (resource.data.audience.roles != null &&
                      request.auth.token.activeHat in resource.data.audience.roles);
      allow write: if isAdmin() || 
                      (isStaff() && hasCapability('KNOW-MANAGE-VAULT'));
    }

Directory Collections

    // --- Directory: Profiles ---
    match /directory_profiles/{profileId} {
      allow read: if isAuthenticated(); // All authenticated users can view directory
      allow create: if isAuthenticated() && 
                       request.resource.data.email == request.auth.token.email;
      allow update: if isOwner(resource.data.userId) || 
                       isAdmin() ||
                       (isStaff() && hasCapability('DIR-MANAGE-PROFILES'));
      allow delete: if isAdmin();
    }

    // --- Directory: Occupancy ---
    match /directory_occupancy/{occupancyId} {
      allow read: if isAuthenticated();
      allow write: if isAdmin() || 
                      (isStaff() && hasCapability('DIR-MANAGE-OCCUPANCY'));
    }

    // --- Directory: Units ---
    match /directory_units/{unitId} {
      allow read: if isAuthenticated();
      allow write: if isAdmin() || 
                      (isStaff() && hasCapability('DIR-MANAGE-UNITS'));
    }

4. Implementation Roadmap

Phase 2.1: Custom Claims Infrastructure (Week 1)

Deliverables: 1. Cloud Function: onUserCreate - Set initial claims on signup 2. Cloud Function: onDirectoryChange - Update claims on relationship changes 3. Server Action: refreshAuthzSnapshot - Manual refresh endpoint 4. Server Action: switchContext - Hat/scope/asset switching

Testing: - Unit tests for graph traversal logic - Integration tests for claims lifecycle - Emulator tests for dirty detection

Phase 2.2: Capability Registry Integration (Week 2)

Deliverables: 1. Migrate capability definitions to CAPABILITY_REGISTRY 2. Update resolveAuthzSnapshot to pull from registry 3. Create capability seeding script for existing users 4. Document capability-to-collection mapping

Testing: - Verify all personas have correct capabilities - Test capability inheritance for sub-facets - Validate delegation scopes

Phase 2.3: Security Rules Implementation (Week 3)

Deliverables: 1. Update firestore.rules with full RBAC rules 2. Implement all helper functions 3. Add collection-specific access patterns 4. Create rules testing suite

Testing: - Emulator tests for each collection - Test matrix: (Role × Collection × Operation) - Negative tests (unauthorized access attempts) - Delegation tests (proxy voting, property management)

Phase 2.4: Migration \u0026 Deployment (Week 4)

Deliverables: 1. Backfill custom claims for all existing users 2. Deploy to staging environment 3. Run comprehensive integration tests 4. Monitor for permission denied errors 5. Deploy to production

Rollback Plan: - Keep development-mode rules as backup - Monitor error rates for 48 hours - Gradual rollout (10% → 50% → 100%)


5. Security Considerations

5.1 Privilege Escalation Prevention

Risk: User modifies client-side context to gain unauthorized access

Mitigation: - Custom claims are server-controlled (cannot be modified by client) - All server actions re-verify claims against Directory - Dirty detection prevents stale claims exploitation - Asset ownership verified server-side

5.2 Delegation Abuse

Risk: Proxy grants used beyond intended scope

Mitigation: - Time-bound delegation (validFrom/validUntil) - Scope-bound delegation (specific capabilities only) - Audit trail for all delegated actions - Revocation mechanism for grants

5.3 Cross-Asset Access

Risk: User accesses data for units they don't own

Mitigation: - ownsAsset() check required for unit-scoped data - availableAssets list maintained in claims - Server-side verification of asset ownership - Branching authorization pattern (admin vs. resident)

5.4 Stale Claims

Risk: User retains access after relationship ends

Mitigation: - authzVersion dirty detection - Automatic claims refresh on Directory changes - Periodic claims re-validation (every 24 hours) - Manual refresh endpoint for immediate updates


6. Performance Optimization

6.1 Snapshot Caching

Strategy: Pre-compute authorization snapshot to avoid N+1 queries

Implementation: - Store snapshot in custom claims (max 1000 bytes) - Compress capability list to IDs only - Cache asset list (limit to 50 assets per user) - Lazy-load full asset details on demand

6.2 Rule Complexity

Strategy: Keep security rules simple and fast

Implementation: - Use helper functions for reusability - Avoid complex nested conditions - Leverage indexed fields for queries - Minimize get() calls in rules

6.3 Token Refresh

Strategy: Minimize forced token refreshes

Implementation: - Batch claim updates (don't update on every field change) - Only refresh on significant relationship changes - Client-side token caching (1 hour TTL) - Background refresh before expiration


7. Testing Strategy

7.1 Unit Tests

Target: Helper functions and graph traversal logic

describe('resolveAuthzSnapshot', () => {
  it('should grant OWNER hat for users with deeds', async () => {
    const profile = createMockProfile({ deeds: ['deed-1'] });
    const snapshot = await resolveAuthzSnapshot(profile);
    expect(snapshot.availableHats).toContain('OWNER');
  });

  it('should grant building scope for STAFF hat', async () => {
    const profile = createMockProfile({ staffAssignment: 'maintenance' });
    const snapshot = await resolveAuthzSnapshot(profile);
    expect(snapshot.availableHats).toContain('STAFF_MAINTENANCE');
  });
});

7.2 Integration Tests

Target: End-to-end claim lifecycle

describe('Custom Claims Lifecycle', () => {
  it('should set claims on user creation', async () => {
    const user = await createTestUser('owner@example.com');
    const claims = await user.getIdTokenResult();
    expect(claims.claims.activeHat).toBe('OWNER');
  });

  it('should update claims on directory change', async () => {
    const user = await createTestUser('owner@example.com');
    await updateDirectoryProfile(user.uid, { deeds: [] });
    await waitForClaimsUpdate();
    const claims = await user.getIdTokenResult(true);
    expect(claims.claims.availableHats).not.toContain('OWNER');
  });
});

7.3 Security Rules Tests

Target: Firestore security rules

describe('Finance Collections', () => {
  it('should allow staff to read accounts', async () => {
    const db = getTestFirestore({ activeHat: 'STAFF' });
    await assertSucceeds(db.collection('finance_accounts').get());
  });

  it('should deny residents from reading accounts', async () => {
    const db = getTestFirestore({ activeHat: 'RESIDENT' });
    await assertFails(db.collection('finance_accounts').get());
  });

  it('should allow owners to read own invoices', async () => {
    const db = getTestFirestore({ 
      activeHat: 'OWNER',
      availableAssets: ['unit-101']
    });
    await assertSucceeds(
      db.collection('finance_invoices')
        .where('assetId', '==', 'unit-101')
        .get()
    );
  });
});

8. Migration from Development Mode

8.1 Transition Plan

Step 1: Parallel Deployment - Deploy custom claims infrastructure - Keep development-mode rules active - Monitor claims generation for all users

Step 2: Gradual Rollout - Enable RBAC rules for 10% of users (feature flag) - Monitor error rates and performance - Expand to 50%, then 100%

Step 3: Full Cutover - Replace development-mode default rule with RBAC rules - Keep development-mode rules as commented backup - Monitor for 48 hours

Step 4: Cleanup - Remove development-mode fallback code - Archive old security rules - Update documentation

8.2 Rollback Procedure

If issues arise during migration:

  1. Immediate Rollback:

    # Restore development-mode rules
    git checkout HEAD~1 -- firestore.rules
    firebase deploy --only firestore:rules --project singular-dream-dev
    

  2. Investigate Issues:

  3. Check Firebase Console for permission denied errors
  4. Review custom claims for affected users
  5. Verify graph traversal logic

  6. Fix and Retry:

  7. Fix identified issues
  8. Test in emulator
  9. Deploy to staging
  10. Retry gradual rollout

9. Future Enhancements

9.1 Field-Level Security

Concept: Restrict access to specific fields within documents

Example:

match /directory_profiles/{profileId} {
  allow read: if isAuthenticated();
  allow update: if isOwner(resource.data.userId) &&
                   // Prevent users from modifying their own roles
                   !request.resource.data.diff(resource.data).affectedKeys()
                     .hasAny(['roles', 'capabilities', 'authzVersion']);
}

9.2 Rate Limiting

Concept: Prevent abuse by limiting operation frequency

Implementation: - Use Cloud Functions to track operation counts - Store rate limit counters in Firestore - Enforce limits in security rules

9.3 Audit Trail Integration

Concept: Log all security rule denials for forensic analysis

Implementation: - Cloud Function triggered on permission denied errors - Store denial events in audit_logs collection - Dashboard for security monitoring

9.4 Dynamic Capability Updates

Concept: Update capabilities without redeploying rules

Implementation: - Store capability definitions in Firestore - Security rules query capability collection - Admin UI for capability management


10. Success Criteria

10.1 Security

  • ✅ No unauthenticated access to protected collections
  • ✅ Role-based access enforced for all collections
  • ✅ Asset ownership verified for unit-scoped data
  • ✅ Delegation properly scoped and time-bound
  • ✅ System collections remain server-only

10.2 Performance

  • ✅ Custom claims generation \u003c 500ms
  • ✅ Security rule evaluation \u003c 100ms
  • ✅ Token refresh \u003c 200ms
  • ✅ No N+1 query issues

10.3 Functionality

  • ✅ All existing features work with RBAC rules
  • ✅ Context switching works seamlessly
  • ✅ Delegation (proxy voting) works correctly
  • ✅ No false denials for legitimate access

10.4 Maintainability

  • ✅ Security rules are well-documented
  • ✅ Helper functions are reusable
  • ✅ Test coverage \u003e 80%
  • ✅ Clear migration path from dev mode

11. References

Internal Documents

Implementation Files

  • packages/foundation/auth/src/types.ts - Role definitions
  • packages/foundation/auth/src/system.ts - Auth system
  • apps/platform/src/lib/context/types.ts - Context state
  • apps/platform/src/lib/auth/graph-resolver.ts - Graph traversal

Current Security Rules


Document Status: Ready for review when roles are finalized
Next Steps: Implement custom claims infrastructure (Phase 2.1)
Owner: Platform Architecture Team
Last Updated: 2026-01-15