insigz docs
Engineering

API & Services

The tenant backend (api) — how requests are shaped, scoped, and served, with one opinionated server per tenant.

The tenant backend (api): how requests are shaped, scoped, and served. One opinionated server per tenant.

Shape

  • TypeScript + Fastify (decided). REST + OpenAPI with a generated client SDK (packages/sdk) keeps the frontend in sync.
  • Stateless Cloud Run service; all state in PG/Mongo/Redis. Horizontal scale by instances.
  • Endpoints are versioned (/api/v1/...).

Middleware chain (every request)

1. TLS/ingress (LB + Cloud Armor)
2. requestId + structured logging
3. JWT verify (**dual-issuer**) → principal  (40-authentication-and-identity.md)
4. authorize(subject, action, resource)  → allow/deny  (41-roles-rights-and-authorization.md)
5. rate limit (Redis, per user/tenant)
6. handler → domain service (opens a SHORT, RLS-scoped DB tx only around DB access)
7. audit write (mutations) + NOTIFY (world-state changes)
8. serialize response

Dual-issuer JWT verification (step 3). The tenant app accepts two token issuers and maps each to a principal type (41):

  • the tenant's own key → a normal tenant user (role + clearance + cell);
  • the control-plane JWKS with purpose:"break_glass" and aud:"tenant:<slug>" → the operator-in-tenant principal (read-only, no tenant role).

A control-plane-signed token must never satisfy the normal-user path: it has no tenant role, so it can only ever land in the break-glass principal (which the engine restricts to reads). Verify aud matches this exact tenant and jti isn't revoked. The RLS context for a break-glass principal sets app.clearance to the operator's authorized level (often OPEN) and never sets a writable role.

Critical: never hold a Postgres transaction/connection across a Claude stream. A chat turn may take seconds; if the request opened a tx in step 3 and held it for the whole model call, the connection pool exhausts under load. Instead, the RLS context (SET LOCAL app.tenant_id/user_id/role/cell) is set on a short-lived tx scoped to each DB access (retrieval, then the post-stream commit of any proposal), and the connection is released around the model/tool calls. Streaming responses (32) run with no DB connection checked out.

Domain services (modules)

canonical (Source/Observation/Entity/Event CRUD + queries), cases, reports (draft + the privileged publish gate), agents (chat, ontology, adjudicator, after-action — ai/30), rag (retrieval/assembly — ai/31), watches/proposals (autonomy — product §13), sources (onboarding/ingest control), admin (members, roles, settings).

Conventions

  • Validation at the boundary (zod/schema); reject unknown fields.
  • Parameterized queries only; no string-built SQL.
  • Idempotency keys (Redis) on mutating POSTs.
  • Pagination cursor-based; AsOf is a query param that caps ingested_at/observed_at.
  • Errors: typed, no internal leakage; every authz deny carries a reason code.

Gaps to close

  • Pooling decided (D18): PgBouncer, transaction-mode in front of each tenant's Cloud SQL (the per-tenant connection budget is small and the realtime LISTEN + ingest compete for it). Transaction-mode requires SET LOCAL (not SET) for the RLS GUCs — already the canonical form (82) — and forbids session-level features across pooled calls. Verify no connection is held across a model/SSE stream.
  • Fill out the full endpoint inventory in 81-openapi-endpoint-inventory.md as domain services land.