Architecture
Tenancy, real-time, and entitlements. The spine the rest of the product sits on top of.
A tenant owns everything: members, projects, policies, billing, audit. A project (workspace) is where the work happens. Every request resolves a tenant from the host header before any business logic runs. The tenant rides the request context through guards, services, queues, and sockets.
The invariants below are the bones. Each one is enforced in code, not just documented. New endpoints inherit the tenant resolver and the membership guard by default. Opting out is an explicit, reviewable change.
Tier_01 // edge
// where the tenant is resolved
Browser
Operations console + auth shells. Tenant resolved from host.
Tier_02 // application
// thin controllers · guards enforce tenancy
Nuxt 3 // SSR
Marketing, auth shells, console pages. Tenant context per request.
Socket.IO // realtime
Tenant + project rooms. Presence, typing, notifications.
NestJS // API
Controllers thin, services do the work. Guards enforce tenancy + entitlements.
Tier_03 // services
// business logic · 30+ modules
Auth + RBAC
JWT, refresh rotation, optional 2FA, tenant role guards.
Entitlements
Plans grant a baseline, policies tighten, add-ons expand. Resolved in one service.
Workspace services
Tasks, chat, meetings, HR, docs, integrations, AI, billing.
Background jobs
Tenant-aware queues. Retention, accrual, storage recalc.
Tier_04 // data plane
// every key, query, and prefix is tenant-scoped
PostgreSQL
Primary OLTP. Real migrations in prod. Tenant-scoped queries.
MongoDB
Chat messages and flex-schema documents.
Redis
Cache (tenant-prefixed keys), rate buckets, pub/sub fabric.
S3-compatible
File plane. Presigned URLs after permission checks. Tenant/project key paths.
Real-time is a shared fabric, not a per-feature add-on. The same Socket.IO and Redis pub/sub pipe carries chat, presence, typing, task updates, meeting state, and notifications. Rooms are explicit. Subscriptions are scoped. The handshake validates tenancy before the connection upgrades.
- [01]
Tenant rooms
Used for tenant-wide signals like presence summary, billing notifications, and announcement broadcasts.
- [02]
Project rooms
Used for project-scoped real-time traffic. Task changes, chat messages, meeting state, document presence.
- [03]
Presence and typing
Lightweight ephemeral state in Redis. Eviction is fast. Reconnect is graceful. No zombie indicators left over.
- [04]
Backpressure
Per-tenant rate buckets for socket emits. Loud tenants do not starve quiet ones.
Plans grant. Policies tighten. Add-ons expand. The entitlements service is the only place that produces an effective cap. The frontend reads it. The guards enforce it. The numbers in the example below are illustrative.
// plan grants. policy tightens. add-on expands. effective is what the API + UI both honor.
- 01
Tenancy resolves before anything else
Every controller resolves a tenant from the host header before any business logic runs. The tenant is attached to the request and to every downstream call. No tenant, no work.
- 02
Tenant scope on every read
Every query carries a tenant filter. The shape of the data model makes cross-tenant joins inconvenient to write. The audit becomes pattern review instead of entity-by-entity proof.
- 03
Cache keys carry the tenant prefix
No cache entry is shared across tenants. Invalidation events scope to the tenant. Noisy-neighbor cache thrash stays inside its own room.
- 04
Socket rooms are scoped, never global
Sockets join explicit tenant and project rooms after handshake validation. There is no global broadcast and no cross-tenant message path. Presence, typing, and notifications all use the same fabric.
- 05
One service computes the effective cap
Plans grant, policies tighten, add-ons expand. The entitlements service produces a single effective answer. Guards read it. Controllers read it. The frontend reads it from the API. No duplicated thresholds.
- 06
Webhooks verify before they mutate
Every inbound webhook verifies its signature before resolving a tenant. Tenant resolution comes before any write. An unknown installation gets no mutation and an audit log entry.