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
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.
// 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"
}
}
{
"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
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);
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)
);
'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);
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
| 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 |
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;
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;
'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
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
esign.signed / req_d9e1f402
attempt 1 of 5
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
actor: plaid_webhook · requestId: a3f7c291
Forwarded to Vectr log-api
escrow-funds-received · done
identification.deadline.started — Day 0 anchored
actor: system · requestId: b5e8d104
Forwarded to Vectr log-api
forty-five-day-id-watcher · running
property.identified — 789 Oak Ave, Santa Monica CA
actor: user_matt_belcher · requestId: c7f9a312
Forwarded to Vectr log-api
property-data-enrichment · done
esign.signed — Exchange Agreement
actor: user_matt_belcher + qi_priya_anand · requestId: d9e1f402
Retry 1/5 — forwarding to Vectr log-api
esign-orchestration · done
compliance.check.triggered — nightly
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
// 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" }
}
// 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