Appearance
Phase B1–B4 — Runtime (shipped)
Builds on Phase B0 foundations.
| Phase | What shipped | Components |
|---|---|---|
| B1 | 202 ACCEPTED + event_id + schema_version: 2; durable audit_events; Redis Streams publish (waspada:events, MAXLEN~) | Event bus, audit logger |
| B2 | In-process pipeline consumer; Redis Streams consumer when DATABASE_URL set (idempotent via processed_events) | Pipeline consumer |
| B3 | Encrypted alerts + corroboration writers (per-row DEK + AAD) | Alert repository, crypto envelope |
| B4 | delivery_outbox enqueue + poller; Redis pub/sub waspada:sse for cross-replica SSE | Outbox poller, SSE publisher |
Correctness notes
- Atomic event claim (ADV-001):
processed_eventsuses a lease-aware lifecycle — claim insertsstatus=in_progress, success flips tocommitted, caught failures delete the claim, andreclaimStaleEventClaims(each outbox poll tick,EVENT_CLAIM_LEASE_MSdefault 60s) frees hard-killed mid-pipeline claims so peers / future XAUTOCLAIM can retry. Inline ingest and the Redis Streams consumer share the same claim — only one wins. Corroborationreport_countbumps only when a newalertsrow is inserted. Outbox idempotency key isfms:${event_id}.alert_idis derived deterministically fromevent_idso SSE retries stay stable. - Outbox lease reaper (ADV-002):
in_flightrows older thanOUTBOX_IN_FLIGHT_LEASE_MS(default 60s) return topending. Attempts increment on failure, not on claim. - Tenant-scoped SSE (ADV-004):
ANALYST_KEYS_JSON(or legacyANALYST_API_KEY+ANALYST_TENANT_IDS) binds analysts to tenants. Explicit["*"](or omittedtenantIds) means all tenants; emptytenantIds: []is deny-all. Stream tickets carry the scope; local listeners and Redis{v:1,tenant_id,payload}envelopes both filter beforesend. Boot parse-validatesANALYST_KEYS_JSON. Durable disposition returns uniform404for unknown and cross-tenant alerts. - XACK after durable work when the Streams consumer runs (requires Postgres idempotency).
- Without
DATABASE_URL, ingest still runs the in-process pipeline (SSE + inline FMS) and returns202. - Gateway PEM remains mandatory in production / hardened (
ALLOW_EPHEMERAL_GATEWAY_KEYSopt-out only outside prod).
Env
| Var | Role |
|---|---|
REDIS_URL | JTI/rate-limit/tickets + Streams + SSE pub/sub |
REQUIRE_REDIS | Refuse memory soft-fail (no JTI split-brain) |
DATABASE_URL + ALERT_DATA_KEK | Durable alerts / audit / outbox |
EVENT_BUS=memory | Force in-process bus (tests) |
FMS_WEBHOOK_URL | Outbox delivery target |
ANALYST_KEYS_JSON | Preferred multi-tenant analyst keys: [{keyId, apiKey, tenantIds}]. Use ["*"] for all tenants; omit tenantIds for the same. Empty [] is deny-all (never default-open). Malformed JSON refuses boot. |
ANALYST_TENANT_IDS | Legacy single-key scope (ANALYST_API_KEY); unset/* = all; empty = deny-all |
OUTBOX_POLL_MS | Outbox poll interval (default 5000) |
OUTBOX_IN_FLIGHT_LEASE_MS | Reclaim stale in_flight rows (default 60000) |
EVENT_CLAIM_LEASE_MS | Reclaim stale processed_events in_progress claims (default 60000) |
OUTBOX_CLAIM_LIMIT | Rows claimed per poll (default 10) |
OUTBOX_BACKOFF_BASE_MS | Retry backoff base (default 60000; soak may use 2000) |
Known gaps (B5+)
| Gap | Why it matters | Plan |
|---|---|---|
No XAUTOCLAIM sweep | If a gateway replica crashes mid-handler, the message stays in the consumer-group PEL until claimed. Event in_progress / outbox in_flight leases are reaped so a future XAUTOCLAIM retry is not defeated. | Periodic XAUTOCLAIM for stuck PEL entries — B5 |
| Inline RDAP on ingest path | Variable latency under the event-claim lease (EVENT_CLAIM_LEASE_MS); move enrichment off the hot path | B2 enrichment worker |
| Audit writes are 1 INSERT per event | Fine for pilot; a 1k ingest burst = 1k round-trips | Batch or tx-buffered audit inserts before harder load-test |
| VAPT / chaos report | External assurance still open | VAPT engagement brief + hire; soak harness in load-test docs |