WorkGrid System Architecture · v0.1.2
Architecture overview

WorkGrid — multi-tenant work-management platform

Monorepo: apps/web (Next.js), apps/api (NestJS + BullMQ worker), packages/shared (contracts). HTTPS-only, multi-tenant via PostgreSQL Row-Level Security, transactional outbox for dual-write safety, real-time over Socket.IO. Read top-to-bottom; arrows show request & data flow.

request path infrastructure bidirectional INV-n = architectural invariant (invariants.json)
Client

🖥 Browser (web)

target_platforms: web · WCAG 2.2 AA
HTTPS / WSSsession cookieX-Org-Id header
HTTPS 443 · WSS
Edge — TLS termination

🔒 nginx reverse proxy

protocol_support: HTTPS only (ADR-0009)
  • HTTP→HTTPS 301 redirect · HSTS (the only plaintext served is the redirect)
  • Routes / → web · /api/ → api (strips prefix → /v1) · /socket.io/ → api (WS upgrade)
  • Header-buffer sizing 16k / 8×32k (large-cookie resilience)
Application tier

apps/web — Next.js 16

App Router · standalone
  • RSC pages + client components; ported landing on copied design tokens
  • TanStack Query (server state) · Zustand (UI state)
  • shadcn-style UI · Tailwind · design tokens (INV-9/17)

apps/api — NestJS 11

REST · URL-path versioning /v1 (INV-2)
Per-request guard + interceptor pipeline:
Throttler
Redis rate limit
JwtAuth
verify token
Tenant
resolve org/role
Rbac
deny-by-default
TenantScope
tx + SET LOCAL GUC
Audit
append-only log
Transform
problem+json

Domain modules apps/api/src/modules

authorgs · members (RBAC)projectstasks (DAG deps) tickets · SLA · escalationworkflows · approvalsdashboardnotifications reports (PDF/CSV/JSON)webhooksintegrationsauditeventshealthoutbox
TypeORM (pooled) · ioredis · Socket.IO adapter
Data & runtime

🛢 PostgreSQL 16

database_preference: PostgreSQL
  • Row-Level Security ENABLE + FORCE per tenant table (INV-14)
  • Policy: organization_id = current_setting('app.current_tenant_id')
  • Optimistic @VersionColumn · unique idempotency keys (INV-7/8/10)
  • Daily backups + WAL/PITR

Redis 7

background_jobs: Bull + Redis
  • BullMQ queues (outbox-relay, sla-escalation, notifications, reports)
  • Distributed rate-limit store (INV-6)
  • Socket.IO pub/sub adapter (multi-pod fan-out)

apps/api worker

separate process · scales independently
  • Outbox relay → delivers side effects idempotently
  • SLA sweep → recompute breaches from due_at
  • Sets tenant GUC per org when touching RLS tables
outbound (signed) · inbound webhooks (HMAC-verified)
External services — no-op adapters until configured

✉ Resend

email · guaranteed delivery via outbox

🐙 GitHub

webhooks (X-Hub-Signature-256) + REST

💬 Slack

signed events (v0=) · incoming webhook
+ Sentry (error reporting) · OpenTelemetry/APM hooks · structured JSON logs with PII redaction (INV-13)
Authenticated mutation — request flow
TLS terminate. nginx forwards; helmet sets HSTS + security headers.
Authenticate. JwtAuthGuard validates the access token → principal.
Resolve tenant. TenantGuard checks membership for X-Org-Id; non-member → NOT_FOUND (no leak).
Authorize. RbacGuard enforces the route's minimum role (deny-by-default).
Scope. TenantScope opens a tx and SET LOCAL app.current_tenant_id so RLS applies.
Execute. Service runs domain logic; external side effects append an outbox_event in the same tx.
Audit. AuditInterceptor writes an append-only audit_log row before commit.
Commit & emit. Worker relays the outbox; EventsGateway emits to room tenant:<org> (INV-15). Response shaped as application/problem+json on error (RFC 9457).
Dual-write safety
domain write
+ outbox row (1 tx)
relay worker
idempotency_key
deliver
retry → DLQ

Transactional outbox (ADR-0007) eliminates "DB committed but webhook lost / webhook sent but DB rolled back". Inbound webhooks verify HMAC over the raw body before any DB read (INV-4) and dedup by (source, external_id).

Tenant isolation (defense-in-depth)

Two enforced layers (ADR-0005): the DB rejects cross-tenant rows via RLS FORCE even if a query forgets the filter; the service layer keeps the GUC contract honest by routing every scoped read/write through the per-request transactional manager. Identity tables (users/orgs/memberships) are intentionally un-scoped so auth can resolve a user before tenant context exists.

Architectural invariants — §9 / invariants.json (lint-enforced)
INV-1 Scoped DB access only via the service/repository layer.
INV-2 Global URL-path versioning; no hardcoded v1 in controllers.
INV-4 Webhook signature verified before processing.
INV-5 process.env only in the config layer.
INV-6 Distributed (Redis) rate limiting configured.
INV-7/8/10 Unique idempotency / tenancy constraints.
INV-9/17 UI uses design tokens; no raw hex.
INV-11 Append-only audit log.
INV-14 RLS ENABLE+FORCE per tenant table.
INV-15 Real-time emits scoped to tenant rooms.
INV-12/13 Structured logs; PII redacted.
INV-16 UI coverage: every screen/endpoint/workflow reachable + e2e.
← Gallery