# 6. Security and multi-tenancy

## 6.1 Tenant isolation (SaaS and on-prem alike)

| Layer | Mechanism | Verified by |
|-------|-----------|-------------|
| Database rows | Every tenant table has `tenant_id`; `tenant_isolation` RLS policy on each (`USING tenant_id = app_current_tenant() OR app_bypass_rls()`), `FORCE ROW LEVEL SECURITY`. The API connects as `wms_app` (NOBYPASSRLS); migrations run as the owner. | `test/e2e/tenancy.e2e-spec.ts` (tenant B cannot read/list/get tenant A rows) |
| Cross-tenant references | Trigger `tenant_ref_guard` on every tenant table rejects an insert/update whose foreign key targets a row of another tenant (migration `0002`). Surfaced as `422 CROSS_TENANT_REFERENCE`. | same suite (writing a SKU against another tenant's storer is rejected) |
| Transactions | `DbService.run()` sets `app.tenant_id` with `set_config(..., true)` (transaction local) at the start of every transaction; a missing tenant without explicit bypass throws before any query runs. | code review, `DbService` unit of work |
| Platform operations | `runBypass()` sets `app.bypass_rls = on` only for platform-admin requests and system jobs; platform tokens are separate (`actorType = PLATFORM_ADMIN`) and tenant users get `403` on `/platform/*`. | tenancy suite |
| Warehouse scope | JWT carries the user's warehouse ids (or `ALL`); `assertWarehouseAccess()` guards every warehouse-scoped call. | inbound/outbound suites use scoped users |
| Storer scope (portal) | Users and API keys bound to a storer are forced to that storer (`STORER_SCOPE` on mismatch). | tenancy suite |
| Object storage | Keys are namespaced `<tenantId>/<category>/...`; documents and attachments are tenant rows. | tenant export lists the keys |
| Export / exit | `GET /platform/tenants/:id/export` produces a zip of NDJSON per table with secrets redacted, for GDPR requests, migration or contract exit. | integration test on the dev stack |

## 6.2 Authentication

- Passwords: argon2id hashes; minimum 10 characters (schema); no password hints; platform admins separate from tenant users.
- Access tokens: HS256 JWT, `JWT_ACCESS_TTL` default 15 minutes, payload = user, tenant, permissions, warehouses, storer, features. Secret `JWT_ACCESS_SECRET` (32+ chars; rotate by deploying a new value, which logs everyone out).
- Refresh tokens: random 256-bit, stored hashed, rotated on every refresh; reuse of a consumed token revokes every session of that user (replay detection), `JWT_REFRESH_TTL_DAYS` default 30, delivered as httpOnly cookie (`COOKIE_SECURE=true` behind TLS) and in the body for native/RF clients; the web app never persists it. Access tokens are checked against their session on every request (30 s cache), so logout, user disablement, password change and tenant suspension take effect immediately.
- API keys: shown once, stored as SHA-256, prefix kept for identification, optional expiry, revocation, per-key scopes, optional storer binding, `lastUsedAt`.
- Rate limiting: global throttler (600 req/min per IP; 30 req/min on `/auth/*`), account lockout after repeated failures (tenant and platform logins), plus per-tenant daily API quota from the plan (`maxApiCallsPerDay`, `429 PLAN_LIMIT`), metered in memory and flushed to `usage_records` every 5 s.
- Outbound requests (webhooks, ERP push) are validated against private/loopback/link-local networks (SSRF) and run with timeouts outside database transactions; `OUTBOUND_ALLOW_PRIVATE_HOSTS=true` relaxes this for on-prem LAN ERPs.

## 6.3 Authorisation

- Permission catalogue in `packages/shared/src/auth/permissions.ts` (`PERMISSIONS`), wildcard per domain (`inbound.*`) and `*`.
- System roles per tenant: ADMIN, OPS_MANAGER, SUPERVISOR, CSO, WAREHOUSEMAN, INVENTORY_CONTROLLER, BILLING, CUSTOMER (portal). Custom roles compose permissions; roles can be assigned per warehouse.
- Separation of duties built in: adjustments need a different approver (`inventory.adjust.request` vs `inventory.adjust.approve`); count variances are posted only by a user holding `inventory.adjust.approve` who did not count the lines; over-receipt override needs `inbound.close`; AI proposals need `ai.decide` (not granted to warehousemen); invoices need `billing.manage`; users/roles need `users.manage`, and only tenant administrators can grant tenant-admin, change storer scope, or hand out permissions they do not hold themselves (no self-promotion).
- Feature flags (`@RequireFeature`) gate paid modules per tenant (AI features, 3PL billing, webhooks, SAP, waves, serials, portal).

## 6.4 Data protection

- Secrets at rest: connector credentials and webhook secrets encrypted with AES-256-GCM (`ENCRYPTION_KEY`, 32 bytes base64); responses mask them.
- Audit log: actor, action, entity, before/after, ip, user agent, request id for every create/update/decision; read-only endpoint `GET /api/audit` (`audit.read`).
- Idempotency and append-only ledger prevent silent double posting; reversals are explicit movements.
- Input validation: every body/query validated by zod (`400 VALIDATION_ERROR` with paths); file uploads limited (15 MB AI intake, 25 MB ERP documents, 10 MB JSON).
- HTTP hardening: helmet headers, CORS allow-list (`CORS_ORIGINS`), request ids on every response, body size limits, no stack traces in responses (logged server side with the request id).
- Logging: pino JSON logs; authorization headers redacted; log level per environment.

## 6.5 Recommended production controls (outside the code)

- TLS termination at the ingress with HSTS; `COOKIE_SECURE=true`.
- PostgreSQL: dedicated instance, `wms_app` password per environment, SSL between API and database, daily backups + PITR, restore drill before go-live.
- Secrets in a vault (Kubernetes Secrets via external-secrets/sealed-secrets, or Docker secrets), never in images or git (`.dockerignore` excludes `.env*`).
- Network: database and object storage not reachable from the internet; API only through the ingress; webhook egress allow-list if required.
- Monitoring: `/api/health` (liveness, DB latency) and `/api/health/ready`; alerts on DEAD outbox events, DEAD webhook deliveries, FAILED integration messages, storage snapshot gaps (the AI billing check also reports them).
- Access reviews: quarterly review of users, API keys (`lastUsedAt`), webhook endpoints, platform admins.

## 6.6 Known limitations to track

- No MFA for users yet (roadmap). Mitigate with strong passwords and SSO at the ingress if needed.
- Reason-code flags `requiresApproval` / `requiresPhoto` and `api_keys.rateLimitPerMinute` are stored but not yet enforced by the API (roadmap).
- API quota is eventually consistent across API replicas (per-node cache refreshed every 60 s, invalidated immediately on the node that changed the subscription).
- Throttling is per IP, not per user; a NAT'd warehouse shares one IP. Raise the limit per site if needed.
