Architecture: How should we unify authentication, session security, and te...
Documentation for Sessions
🏗️ Architecture: How should we unify authentication, session security, and te...
Date: 2026-03-16
Time: 19:09:35 UTC
Session: gspace1-226.4
Agent: backend
Confidence: 88%
Status: ✅ Documented
📋 Decision Context
Question:
How should we unify authentication, session security, and tenant isolation across all 4 backends (backend-api, backend-ai-api, backend-vitto-api, backend-docs-api)?
Task: Design unified auth, session security, and tenant isolation for all 4 backends
🎯 Decision Made
Chosen:
Full APP_GUARD unification with phased rollout: (1) UnifiedAuthGuard registered as APP_GUARD on all backends (deny-by-default), (2) Dedicated /internal/* routes for service-to-service communication (Option B), (3) tenantId=spaceId formalized across all 8 session schemas, (4) Whistle hardening with proper auth + tenant isolation, (5) WebSocket auth guard, (6) SSE ticket-based auth pattern.
Options Considered:
- Add guards to 70 individual controllers one by one (no global guard)
- APP_GUARD on ai-api and vitto-api only, keep BackendAuthGuard on backend-api
- Full APP_GUARD on all backends with phased per-backend rollout (chosen)
- Dual-mode endpoints (same route serves users and services with different auth logic)
💭 Reasoning
Root cause: No backend registers a global guard. 70/150 controllers (47%) are completely unguarded. 5 distinct guard implementations set incompatible request.user shapes (sub vs userId). Whistle endpoints are @Public() with self-asserted caller identity. tenantId exists in schemas but is never populated. Inter-service tokens use inconsistent variable names.
Why full APP_GUARD over alternatives:
- Adding guards to 70 individual controllers is error-prone and doesn't prevent future regressions (new controllers inherit the unguarded default)
- Partial rollout (APP_GUARD on some backends) creates inconsistent security model
- Full rollout with phased per-backend audit (S227.2a-d) minimizes blast radius while achieving deny-by-default
Why Option B (internal routes) over Option A (dual-mode endpoints):
- Pre-production = ideal time for foundational changes
- Separating user-facing and service-facing routes eliminates conflicting authorization logic
- Vitto's Jarvis-like intelligence needs cross-tenant aggregation that is fundamentally incompatible with per-user scoping on the same endpoint
Why tenantId=spaceId (not a separate tenant entity):
- A tenant is a company/space. Companies rarely own other companies. Adding a meta-tenant layer would over-complicate queries.
- Users can belong to multiple tenants (spaces) - this is handled by the active space context, not nested tenancy.
Critical de-risking: Rollback is a single-line revert of APP_GUARD registration. @Public() annotations are harmless without a global guard. Internal endpoints are additive (new controllers).
Key risks addressed: (1) SSE endpoints can't send headers → ticket-based auth pattern, (2) WhistleOrchestratorService.checkSpaceMembership() lacks auth headers → fixed before APP_GUARD goes live, (3) Mongoose pre-save hooks don't fire on updateMany → service-layer enforcement as primary mechanism, (4) X-Tenant-Id header only honored for service callers to prevent user-side tenant override.
📁 Files Involved
libs/libs-api/auth-core/src/lib/guards/jwt-auth.guard.tslibs/libs-api/auth-core/src/lib/auth-core.module.tsAPIs/backend-api/src/app/backend-auth/backend-auth.guard.tsAPIs/backend-ai-api/src/app/infrastructure/guards/infrastructure-auth.guard.tsAPIs/backend-ai-api/src/app/sessions/sessions.controller.tsAPIs/backend-ai-api/src/app/sessions/sessions.schema.tsAPIs/backend-vitto-api/src/app/engine/whistle-orchestrator.service.tsAPIs/backend-vitto-api/src/app/engine/vitto.controller.tsAPIs/backend-vitto-api/src/app/mind/schemas/agent-session.schema.tsdocs/superpowers/specs/2026-03-16-sessions-security-unification-design.md
🔗 Related
- Decision ID:
dev_1773688175247_8u7bu47 - Session: 226
- Workspace: gspace1
Auto-generated by MCP Smart Consolidation