Recon — System Architecture

Multi-tenant external attack-surface monitoring SaaS. Modular-monolith NestJS API + Next.js web, Postgres with Row-Level Security, BullMQ on Valkey, and an egress-isolated scan worker. HTTPS-only edge.

v0.1.1 · SOC2 · HTTPS-only
← back to app
trust boundary · egress-isolated scanner network SOC Analyst (Browser) dashboard · live WS feed nginx edge — TLS HTTPS only · HTTP→HTTPS · HSTS (INV-11) Next.js Web App Router · shadcn · TanStack NestJS API /v1 auth · assets · scans · changes · alerts JwtAuthGuard · RolesGuard · Throttler · TenantScope PostgreSQL 17 Row-Level Security (INV-2/3) Valkey BullMQ · cache · ws Scan Worker native TCP/TLS engine SSRF guard (INV-4) External authorized targets Outbox relay at-least-once (INV-6/7) Slack Block Kit Webhook HMAC (INV-8) Resend email Vault KV v2 secrets (INV-9) Sentry errors HTTPS / WSS / /api → /v1 REST TypeORM · SET LOCAL tenant enqueue scan job deny RFC1918/metadata snapshot · diff · change events outbox rows WS events
service / datastore
HTTPS edge
egress-isolated scanner
security guards
primary request / data path
async / side-channel

Core data flow

  1. Analyst registers an asset (domain / CIDR); it starts pending.
  2. Authorization-to-scan proven via DNS-TXT / HTTP-file → authorized (INV-5).
  3. Scan triggered (manual inline / BullMQ schedule); per-asset Redis lock prevents overlap.
  4. Scan worker resolves targets through the SSRF guard, probes ports + TLS certs.
  5. Normalized snapshot persisted (JSONB) and hashed for cheap equality.
  6. Diff vs accepted baseline → typed change_events (PORT_OPENED, CERT_ROTATED…).
  7. Each event + an outbox row written in one ACID transaction (dual-write safe).
  8. Outbox relay delivers to Slack / signed webhook / email; WS broadcasts to the tenant room.
  9. Everything appended to the immutable audit log; data feeds PDF/JSON/CSV reports.

Security boundaries & invariants

INV-2/3Tenant isolation — Postgres RLS (FORCE) gated on a transaction-local app.tenant_id; the app connects as a non-superuser role.
INV-4SSRF guard — every scan target & webhook URL denies RFC1918 / loopback / link-local; metadata IP denied unconditionally; re-resolved at exec.
INV-5Authorization-to-scan — a scan cannot run against an unverified asset.
INV-6/7Transactional outbox with UNIQUE(event_id, channel_id) idempotency; ≤5 retries → dead-letter.
INV-8Webhook signing — HMAC-SHA256 computed before the HTTP request.
INV-9Secrets via Vault KV v2 (env fallback for local); argon2id passwords; no secrets in logs.
INV-11/12HTTPS-only edge + HSTS; append-only audit log enforced by a DB trigger.

Technology stack

NestJS 11
REST /v1 · WS
TypeORM
migrations · RLS
PostgreSQL 17
RLS · JSONB
Valkey + BullMQ
jobs · schedules
Next.js 15
App Router
shadcn · Tailwind
design tokens
TanStack Query
server state
@recon/scanner
native TCP/TLS
nginx
TLS edge
Vault · Sentry
secrets · errors
Resend · Slack
alert sinks
Docker Compose
local stack
← Gallery