Exchange Event Bus
Outbound taxonomy · Inbound HMAC · Idempotency · Audit forwarding
Bus operational
api-health-checks · live error-alerting-retry · live
2,847 events/day
99.96% delivery
HMAC: all verified
2 in retry queue
Audit log lag: 42ms
Exchange #1087 · Matt Belcher · Day 31 of 180
Neela — Event Bus Overview
Open1031 fires 28 named events to Vectr's event router on state changes. Every event carries a requestId (UUIDv4), exchangeId, and occurredAt. Vectr workflows subscribe by event type and fire the right automation — zero polling, zero manual triggers.
Event Flow Architecture
Open1031 Pages Functions enqueue events; Vectr router fans out to workflow subscribers
All systems nominal
Open1031
Cloudflare Pages Functions
POST /exchanges/:id/events Supabase trigger Hub wizard submit
outbound
JSON + sig
Vectr Event Router
Restate Cloud — durable dispatch
topic fanout dead-letter exp backoff retry
dispatch
per subscriber
Vectr Workflows
Automation · AI · Infra · Security
forty-five-day-id-watcher escrow-funds-received kyc-orchestration + 25 more
inbound signed webhook · POST /api/vectr/webhook · HMAC-SHA256 verified
Outbound Event Taxonomy
28 named events fired by Open1031 to Vectr. Every payload includes requestId · exchangeId · tenantId · occurredAt · actorId · version
Outbound
Event Type Phase Triggered By Vectr Workflow Subscriber(s)
exchange.created Initiation Exchanger submits onboarding wizard exchange-record-create kyc-orchestration qi-engagement-generate
kyc.initiated Initiation Exchange created; Persona flow started kyc-orchestration
kyc.cleared Initiation QI reviews and clears Persona result kyc-orchestration ofac-sdn-screening
kyc.failed Initiation Persona result failed or QI rejected kyc-orchestration compliance-risk-checks
ofac.screened Initiation OFAC SDN check completed (clear or hit) ofac-sdn-screening
documents.required.computed Initiation Taxpayer type change or wizard start required-documents-generator
relinquished.property.saved Sales Exchanger saves property address wizard-state-autosave property-data-enrichment
escrow.title.saved Sales Escrow/title company data entered escrow-title-validation
wire.instructions.sent Escrow QI authorizes outbound wire instruction escrow-wire-routing
escrow.funds.received Escrow Bank webhook / Plaid confirms proceeds landed escrow-funds-received
esign.envelope.sent Sales DocuSign envelope dispatched esign-orchestration
esign.signed Sales All parties have signed esign-orchestration document-verification-merge
exchange.sale_closed Sales Sale hub submitted; relinquished property transfers at closing — arms both statutory clocks forty-five-day-id-watcher one-eighty-day-deadline-guard
property.identified ID Exchanger adds a replacement property wizard-state-autosave property-data-enrichment dashboard-kpi-update
identification.notice.signed ID Exchanger e-signs Identification Notice identification-notice-generate esign-orchestration
identification.deadline.missed ID Day 45 00:00 passed without signed notice forty-five-day-id-watcher compliance-risk-checks
replacement.closing.opened Purchase Exchanger opens Purchase Hub for a property escrow-title-milestone-tracker document-collection-reminders
replacement.closing.recorded Purchase Closing recorded; post-closing docs requested post-closing-document-requests
wire.release.requested Purchase Exchanger requests a permitted release; an authorized officer proposes the wire escrow-wire-release
wire.release.approved Purchase Multi-party approval completed (2-of-N) escrow-wire-release
acquisition.deadline.missed Purchase Day 180 00:00 passed without acquisition one-eighty-day-deadline-guard compliance-risk-checks
exchange.completed IRS All acquisition data confirmed complete final-audit-archive form-8824-generate
form8824.ready IRS Form 8824 pre-fill completed by Vectr AI form-8824-generate
archive.exported IRS Exchange ZIP archive created in R2 final-audit-archive
document.uploaded System Any document upload to Supabase Storage document-verification-merge
wizard.field.changed System Debounced 5 s form field change wizard-state-autosave
compliance.check.triggered System Section submit or nightly 00:30 compliance-risk-checks
Canonical Event Payload
Every outbound event shares this envelope. data is event-type-specific.
v2 schema
// POST /exchanges/:id/events — Hono route in Pages Functions
{
  "eventType": "escrow.funds.received",
  "requestId": "a3f7c291-8b2e-4d1a-9c3f-00e1b2e3f4a5", // UUIDv4 — idempotency key
  "exchangeId": "exch_1087_mb",
  "tenantId": "tenant_open1031_default",
  "occurredAt": "2026-05-15T14:32:07.841Z",
  "actorId": "user_matt_belcher",
  "actorType": "exchanger",
  "version": "2",
  "data": {
    "amount": 850000,
    "currency": "USD",
    "escrowAccountId": "esc_1087_segregated",
    "confirmedBy": "plaid_webhook_evt_xyz"
  }
}
Inbound Security — HMAC-SHA256 Required on Every Webhook
Every inbound webhook to POST /api/vectr/webhook must carry a valid X-Vectr-Signature header. Any request that fails timing, replay, or HMAC validation returns 401 immediately — no partial processing, no audit log written for the attempt.
HMAC Verification — 6-Step Guard
Executed synchronously in the Hono middleware before any handler runs
Required
1
Extract timestamp + signature
Parse X-Vectr-Signature: t=1748123527,v1=abc123… — reject immediately if either part is missing or malformed.
2
Replay window guard — 5 minutes
Reject if |now − t| > 300s. Prevents delayed replay even if the signature itself would verify.
const age = Date.now() / 1000 - t;
if (Math.abs(age) > 300) return c.json({error:'replay'}, 401);
3
Build the signed payload string
Concatenate timestamp + raw body bytes: signingPayload = t + "." + rawBody. Must use the raw body bytes, not a re-serialised JSON object.
4
Compute expected HMAC-SHA256
Using VECTR_WEBHOOK_SECRET from Cloudflare environment (never in source).
const expected = await crypto.subtle.sign(
  'HMAC', secretKey,
  encoder.encode(signingPayload)
);
5
Constant-time comparison
Use crypto.subtle or equivalent timing-safe compare — never === on hex strings (timing oracle).
const match = timingSafeEqual(
  hexToBytes(v1), new Uint8Array(expected)
);
if (!match) return c.json({error:'invalid_sig'}, 401);
6
Dispatch to handler + write audit log
Verification passed. Call the event-type handler. Write the audit entry before any side effects — audit log is never skipped on a verified event.
Inbound Webhook Registry
7 event types Vectr fires back to Open1031
Inbound
Event Type Source Workflow Open1031 Handler
kyc.result.received kyc-orchestration Update KYC status in Supabase; unblock hub progress
esign.completed esign-orchestration Mark document signed; advance hub step
deadline.alarm.fired forty-five-day-id-watcher one-eighty-day-deadline-guard Push notification + in-app alert; escalate if Day 44/179
wire.release.approved escrow-wire-release Record multi-party approval; update disbursement hub
form8824.generated form-8824-generate Store PDF in Supabase Storage; notify exchanger + CPA
compliance.risk.flagged compliance-risk-checks Write risk finding; surface in QI admin dashboard
archive.export.ready final-audit-archive Notify exchanger; expose download link in Documents vault
Signature mismatch — error state
If HMAC verification fails: return 401 {"error":"invalid_signature"}. Do NOT write an audit log (no verified actor). Write a platform security alert to the QI ops dashboard. Never surface the expected hash in the response body.
Happy path — verified
HMAC verified in 0.4ms2026-06-06 14:32:07Z
Audit entry written14:32:07.004Z
Handler returned 20414:32:07.019Z
Loading state — in-flight
Parsing X-Vectr-Signature header…
Error state — replay rejected
Timestamp age: 412s > 300s14:37:19Z
Returned 401 — replay blockedNo audit entry written
Security alert queued to QI ops dashboard
Idempotency — Why It Matters for a Regulated QI
Network retries, Vectr's at-least-once delivery, and Cloudflare Worker re-invocations mean the same event can arrive more than once. Without idempotency, an exchanger's funds could be double-moved or a deadline watcher could start twice. The requestId is the single idempotency key — persisted before any side effect.
Idempotency Mechanics
Applied identically to outbound event dispatch and inbound webhook handling
Persist before side effects
Write the requestId + eventType + "processing" status to event_log in a single Supabase transaction before calling any downstream service. If the Worker is killed mid-flight, the record exists and the retry check will short-circuit.
INSERT INTO event_log (request_id, event_type, status, occurred_at)
VALUES ($1, $2, 'processing', now())
ON CONFLICT (request_id) DO NOTHING
RETURNING id;
Check on every arrival
On any inbound request, query event_log for the requestId before processing. If status is "completed" return 200 with the cached response body — no re-execution, no duplicate side effects.
const existing = await db.query(
  'SELECT status FROM event_log WHERE request_id = $1', [requestId]
);
if (existing.rows[0]?.status === 'completed') return cached;
Partial-failure recovery
If status is "processing" on retry, the Worker checks whether side effects completed (e.g. audit log written, Supabase row updated) before re-attempting. Never re-attempt a money-movement step — escalate to QI ops instead.
TTL + cleanup
event_log rows with status="completed" are retained for 90 days (SOC 2 audit evidence). After 90 days, archived to cold Supabase partition. The api-health-checks workflow monitors for stuck "processing" rows > 30 min.
Critical Event Idempotency
Events where duplicate processing is compliance-critical
Event If Duplicate Guard
escrow.funds.received Double-generates escrow receipt confirmation requestId dedup
wire.release.approved Double-wire disbursement requestId + QI gate
identification.deadline.started Resets Day 0 anchor requestId dedup
exchange.created Duplicate exchange record DB unique on exchangeId
form8824.generated Duplicate PDF stored requestId dedup
event_log Table Schema
-- Supabase: idempotency + audit ledger
CREATE TABLE event_log (
  id           uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  request_id   uuid NOT NULL UNIQUE, -- idempotency key
  exchange_id  text NOT NULL,
  tenant_id    text NOT NULL,
  event_type   text NOT NULL,
  direction    text CHECK (direction IN ('outbound','inbound')),
  status       text CHECK (status IN ('processing','completed','failed')),
  actor_id     text,
  payload      jsonb, -- NPI scrubbed before storage
  occurred_at  timestamptz NOT NULL,
  processed_at timestamptz,
  vectr_forwarded boolean DEFAULT false,
  retry_count  int DEFAULT 0
);
-- RLS: tenant isolation enforced by auth.uid() policies
Audit Forwarding — SOC 2 + QI Books-and-Records
Every verified state change is forwarded to Vectr's log-api Durable Object for tamper-evident, long-term storage. The Supabase event_log table is the primary local ledger; Vectr is the immutable secondary. Discrepancies between the two trigger a compliance-risk-checks alert.
2 entries pending in audit retry queue error-alerting-retry · live
escrow.funds.received / req_a3f7c291 attempt 2 of 5 Next retry in 47s · Exp backoff
esign.signed / req_d9e1f402 attempt 1 of 5 Next retry in 2m 14s · Exp backoff
After 5 failed attempts, entries move to dead-letter queue and QI ops is alerted. The Supabase record remains intact — Vectr forwarding is additive, never the primary record.
Live Audit Feed — Exchange #1087
Matt Belcher · 456 Sunset Blvd, Los Angeles CA · $850,000
Live data
escrow.funds.received — $850,000 confirmed
Jun 6, 2026 · 14:32:07Z
actor: plaid_webhook · requestId: a3f7c291
Forwarded to Vectr log-api escrow-funds-received · done
identification.deadline.started — Day 0 anchored
Jun 6, 2026 · 14:32:08Z
actor: system · requestId: b5e8d104
Forwarded to Vectr log-api forty-five-day-id-watcher · running
property.identified — 789 Oak Ave, Santa Monica CA
Jun 6, 2026 · 15:14:22Z
actor: user_matt_belcher · requestId: c7f9a312
Forwarded to Vectr log-api property-data-enrichment · done
esign.signed — Exchange Agreement
Jun 6, 2026 · 15:47:03Z
actor: user_matt_belcher + qi_priya_anand · requestId: d9e1f402
Retry 1/5 — forwarding to Vectr log-api esign-orchestration · done
compliance.check.triggered — nightly
Jun 6, 2026 · 00:30:00Z
actor: system_cron · requestId: e2a8c514
Forwarded to Vectr log-api compliance-risk-checks · done
Vectr log-api Contract
Durable Object — append-only, HMAC-signed entries
log-api · live
// POST https://api.vectr.ai/log-api/entries
// Header: Authorization: Bearer VECTR_LOG_API_TOKEN
// Header: X-Open1031-Sig: HMAC-SHA256 of body
{
  "requestId": "a3f7c291-…",
  "exchangeId": "exch_1087_mb",
  "tenantId": "tenant_open1031_default",
  "eventType": "escrow.funds.received",
  "actorId": "plaid_webhook",
  "resourceType": "escrow_account",
  "resourceId": "esc_1087_segregated",
  "npiTokenized": true, // NPI scrubbed — token only
  "occurredAt": "2026-06-06T14:32:07.841Z",
  "meta": { "amount": 850000, "currency": "USD" }
}
NPI Scrubbing Before Forwarding
SSN stored as last-4 only in Supabase profiles_sensitive. Never included in any event payload or forwarded log entry.
DOB, full SSN, bank account numbers replaced with opaque tokens before event payload construction.
npiTokenized: true flag on every forwarded entry certifies scrubbing completed.
GLBA Safeguards Rule: log-api token + HMAC signature protects NPI in transit even for the non-sensitive fields.
Error state — Vectr log-api unreachable
If the Vectr log-api endpoint returns 5xx or times out: (1) the local Supabase write already succeeded — the exchange record is safe; (2) the entry is queued in the retry table with exp backoff; (3) after 5 failures, QI ops is alerted; (4) the exchange is NOT blocked — forwarding is async and non-blocking.
Audit lag > 5 min — QI ops alerted error-alerting-retry · running