ELA · END-TO-END FLOWS · v2 (2026-08)

End-to-end flows, by stakeholder

How a live case moves across every party — who is contacted, in what order, with what SLA. v2 refreshes the v1 (June 2026) diagram against the shipped code: watch arming, emirate-based firm matching, the honest "ops handles directly" rung, no-verification trusted contacts, and the current UK-VPS residency reality. v1 kept for reference →

The whole journey

Four phases — eligibility, activation, dispatch, resolution — with ops and the append-only audit ledger running across all of them. Arming works 24/7 with no human in the loop; ops act only on exceptions.

Client (phone / watch) System / ELA Ops Law firm Consulate Insurer

Master flow

flowchart TB
  subgraph P1["1 · Eligibility (straight-through)"]
    A1[Onboarding questions]-->A2["Capture passport + EID / employment letter"]
    A2-->A3{"On-device check
blur, corners, glare, MRZ"} A3-->|fail|A2 A3-->|pass|A4["Upload to ELA VPS (filesystem store)"] A4-->A5{"Server pre-screen
score, classify, OCR"} A5-->|pass STP|A6[Advance to payment] A5-->|flag|OPS1[("Ops exception queue
+ resubmission loop")] end subgraph P2["2 · Activation"] A6-->B1[Choose tier] B1-->B2{"Card (Stripe) / Tabby — no IAP"} B2-->|success|B3["Active · entitlement set (own Postgres table)"] B2-->|fail / cancel|B1 B3-->B4["Shield/Family: ops assigns policy number
30-day Profile Verification window"] end subgraph P3["3 · Emergency"] B3-->C0{"Arm from…"} C0-->|phone|C1["Hold-to-slide arm"] C0-->|Apple Watch|CW["Wrist arm → phone-relay
(independent direct path server-ready, unverified)"] CW-->C1 C1-->C2["Pre-dispatch hold
slide to cancel (~10s)"] C2-->C4["Dispatch fires
+ background location streams (realtime)"] end subgraph P4["4 · Dispatch and resolution"] C4-->D1["Cycle firms in member's emirate
(engine supports N; launch = 1/emirate)"] C4-->D2["Notify consulate(s) for nationality"] C4-->D3["Notify ALL entered trusted contacts"] D1-->D4{Accept within SLA?} D4-->|no|D5["Escalate email+SMS, cc ops
+ grace, then next firm"] D5-->D1 D4-->|none available / exhausted|OPD["Ops handles directly
member sees 'Our team is handling this directly'"] D4-->|yes|D6["Firm confirms client contact (2h)
member sees firm name + 'lawyer calls you'"] OPD-.->|ops can hand back|D1 D6-->D7{Shield/Family insured?} D7-->|yes, post-window|E1[Ops sends insurance pack] E1-->E2["Firm initiates claim
100k legal + 50k bond"] D7-->|no|E3["Case proceeds, notification-only"] end C4-.->LED[("Notification ledger
append-only, member-viewable")] D2-.->LED D3-.->LED

Live case — timing

sequenceDiagram
  participant U as Client (phone / watch)
  participant S as ELA System
  participant F as Law firm (emirate)
  participant C as Consulate
  participant T as Trusted contacts
  participant O as Ops
  participant I as Insurer
  U->>S: Arm (hold-to-slide) from phone or watch via phone-relay
  Note over U,S: Watch Phase-2 wrist arms via phone-relay — direct path server-ready but hardware-unverified
  S->>F: Notify firm in member emirate (cycles until one accepts)
  S->>C: Notify consulate(s) for nationality (audited email)
  S->>T: Notify ALL entered trusted contacts (SMS/WA) no verification
  S-->>U: Live status timeline plus background location streaming
  Note over S,F: Firm SLA 12h in-week / 24h weekend or holiday plus grace
  alt Firm accepts in SLA
    F->>S: Accept then confirm client contact (2h)
    S-->>U: Lawyer assigned — your lawyer calls you
  else SLA breach or declines
    S->>F: Escalate email+SMS (cc ops) plus grace then next firm
  else None available or all exhausted
    S->>O: Ops handles the case directly
    S-->>U: Our team is handling this directly
  end
  opt Shield / Family insured (post 30-day window)
    O->>F: Send insurance pack (policy + limits + contact)
    F->>I: Initiate claim
  end
  Note over S,T: Pre-launch — firm and consulate and contact comms run in SIMULATE mode until providers and enable flags land
  

Member-facing sequence is server-authoritative: firm names appear only on a real server-confirmed accept (never invented); no_firm_available renders the honest ops-direct rung, not a fake "searching". Source: packages/cases, apps/api/src/server.ts dispatch handler, apps/mobile/src/services/activeCaseTimeline.ts.

Client & family buyer

The person who may be detained — and the family member who can buy on their behalf. The account holder completes document capture; coverage attaches to a verified identity. The arm can be triggered from the phone or the paired Apple Watch.

flowchart TB
  subgraph ON["Onboarding (11-step funnel)"]
    W[Welcome]-->HOW[How it works]
    HOW-->ACC["Account + OTP"]
    ACC-->PER["Identity: name, DOB, gender"]
    PER-->ADDR["Residence emirate"]
    ADDR-->PRI["Priors (sets insurance eligibility)"]
    PRI-->PLAN["Plan picker"]
    PLAN-->PAY{"Card (Stripe) / Tabby — no IAP"}
    PAY-->|success|PROT["Protection moment"]
    PAY-->|fail|PLAN
    PROT-->REH["Rehearsal: hold, slide-cancel, slide-end"]
    REH-->DOC["Documents offer: passport + EID or letter"]
    DOC-->CHK{"On-device check passes?"}
    CHK-->|retry|DOC
    CHK-->|3 tries used|ESC["Gallery upload or continue → ops"]
    CHK-->|pass|DONE2[Set up]
    ESC-->DONE2
  end
  DONE2-->ACT["Active (notification tier live immediately)"]
  ACT-->PVW["Profile Verification Window 30d
insurance live day 31 if verified + policy"] ACT-->ARM0{"Detained — arm from…"} ARM0-->|phone|ARM["Hold-to-slide"] ARM0-->|watch|ARMW["Wrist arm → phone-relay"] ARMW-->ARM ARM-->HOLD["Pre-dispatch: slide to cancel (~10s)"] HOLD-->DISP[Dispatch fires] DISP-->LOC["Background location streams (realtime)
until case-end or battery dies"] DISP-->TL["Live STATUS TIMELINE (not chat-first):
reaching → firm assigned / ops-direct"] TL-->MSG["'Message us' → one-tap chat, or ops phone backup"] DISP-->ENDC["Slide to end (biometric/PIN)"]

Receives

Live on-device capture feedback; in-app access; policy number once assigned; during a case, a server-authoritative status timeline of who is engaged, plus a one-tap "Message us" to ops and an ops phone backup.

Expected of them

Onboard honestly; keep passport/EID/visa current (app reminds + prompts re-upload via the resubmission loop). Choose a tier.

Pricing · 3 tiers

1,750 Essential · 3,250 Shield (centre-stage) · 7,000 Family Shield. AED/year, no trial. Paid via card (Stripe) / Tabby — not IAP (insurance carve-out 3.2.1(v)). 30-day money-back.

Failure handling

Junk doc → rejected in-app, resubmit. Doc later flagged → automated resubmit request (Direction-3 sheet); tier not revoked unless fraud. Payment fails → no access until completed.

Likely friction (pre-empt in messaging)

"Why annual, no trial?"

Committed life-safety product; the single arm is the moment of truth.

"Where does my passport live?"

On the ELA server, filesystem storage with custom HMAC-signed URLs, no third-party cloud OCR. Note (internal): the prod VPS is currently Linode London — the UAE-residency requirement was scrapped 2026-06-12; S3 me-central-1 remains pending infra, not deployed. Residency framing is a live decision.

"Arrested on day 3?"

Dispatch is active immediately at payment; only the insurance claim waits out the 30-day window.

"Can I arm from my watch?"

Yes — the Apple Watch arms via the phone (relay-primary). A phone-independent direct path is built and server-side, but not yet hardware-verified.

Trusted contact

Nominated during onboarding. No app, no account. An informational recipient and human rallying point — not a decision-maker. No verification: every entered contact is notified at arm-time; a mistyped number is a known, founder-accepted leak (entry alone is sufficient).

flowchart LR
  NOM["Nominated at onboarding (no verification step)"]-->IDLE["Stands by (no app)"]
  IDLE-->TRIG["Subscriber arms a case"]
  TRIG-->MSG["ALL entered contacts receive SMS / WhatsApp:
subscriber detained, firm + consulate engaged"] MSG-->DEL{Delivered?} DEL-->|yes|ACTC["May contact family or firm"] DEL-->|no|OPSC["Failure logged, visible to subscriber + ops"]

Receives

An outbound alert per triggered case (dark "response arc" framing). Every message logged immutably.

Expected of them

Nothing contractual. An emergency informant, not on the hook to post bail.

No verification (by decision)

Contacts are NOT verified — notify all entered at arm-time. Founder accepts the mistyped-number risk; a life-safety alert should never be blocked on a verification handshake.

Failure handling

Bad number → delivery status recorded, visible to subscriber + ops board.

Law firm

Selected by the cycling engine from firms covering the member's emirate (resolved from the case location → residence emirate → legacy Dubai default; no geocoder yet). The engine supports N firms; launch runs one active firm per emirate. SLA is computed from each firm's own configured working hours.

12h · inside working week 24h · weekend / holiday + escalation grace accept = take the case emirate-matched
flowchart TB
  A["Resolve member's emirate → pick a covering firm"]-->N["Firm notified via portal + client context"]
  N-->SLA{"Respond within SLA?
12h in-week / 24h weekend or holiday"} SLA-->|no|ESCF["Escalate: email + SMS, ops cc, + grace"] ESCF-->NX["Advance to next firm in emirate"] NX-->EX{Firms left?} EX-->|yes|N EX-->|none|OPD["Ops handles directly
member: 'Our team is handling this directly'"] SLA-->|accept|CONF{"Confirm client contact within 2h?"} CONF-->|no|CHASE["Ops chases, then re-dispatch"] CHASE-->NX CONF-->|withdraws|NX CONF-->|yes|WK["Work the case (member sees firm name + 'lawyer calls you')"] WK-->T{Shield/Family insured?} T-->|yes, post-window|PK["Receive insurance pack from ops"] PK-->CL["Initiate claim: 100k legal + 50k bond"] T-->|no|WORK["Notification-only case"] OPD-.->|ops can hand back|N

Receives

Case notification + client context via portal. On breach: escalation email+SMS (ops cc'd). For accepted insured cases: insurance pack (policy number, 100k/50k limits, claims contact).

Emirate matching

Firm pool is filtered to the member's emirate. No geocoder yet — the location rung passes null, so residence emirate decides (then legacy DXB). 7 per-emirate test firms live on prod.

Single-firm launch

The round-robin engine is real, but launch config is one firm per emirate. Copy simplifies the multi-firm promise; the failover ("finding another firm") screens are kept for when the pool grows.

Failure handling

Miss SLA → auto escalate + grace → next firm. None available → ops handles directly (not a dead end — ops can resolve, or hand back to a firm).

Consulate

Notified automatically when a national triggers a case. View-only. No portal, no required action — awareness and consular duty-of-care.

flowchart TB
  TRIG[Case triggers]-->NAT{Nationalities to notify}
  NAT-->P["Primary nationality"]
  NAT-->|if added|S2["Second nationality"]
  P-->Q1{Consulate in UAE?}
  S2-->Q2{Consulate in UAE?}
  Q1-->|yes|E1["Auditable email: detained, being assisted"]
  Q1-->|no|NF1["Nearest consulate + ops flag (labelled)"]
  Q2-->|yes|E2["Auditable email"]
  Q2-->|no|NF2["Nearest + ops flag"]
  E1-->DEL{Delivered?}
  E2-->DEL
  DEL-->|yes|STORE["Stored: body + time + message-id (member-viewable)"]
  DEL-->|bounce|RETRY["Ops alert + retry + alternate contact"]
  

Receives

A single auditable email. Full body + timestamp + delivery status + message-ID persisted as evidence.

Expected of them

None enforced by ELA. Strictly informational; ELA does not direct the consulate.

Pre-launch

The consulate email path runs in SIMULATE mode until the email provider + enable flag land — audit rows read "simulated", never "delivered".

Failure handling

No UAE consulate → nearest + ops flag, labelled in the audit trail.

Insurance company

Provides a policy number per insured subscriber; covers legal costs and bond up to 150,000 AED. Underwriter Sukoon + reinsurer Howden (names feed the admin underwriter config; app copy stays generic). The exact policy-number handoff is still being defined.

flowchart TB
  SU["Shield / Family Shield purchase"]-->SEND["Ops sends client details + payment"]
  SEND-->ISSUE["Insurer issues policy number"]
  ISSUE-->WIN["ELA: 30-day Profile Verification Window"]
  WIN-->LIVE{"Day 31 and docs verified?"}
  LIVE-->|claim before|BLOCK["Claim blocked (in window)"]
  LIVE-->|yes|COV["Insurance live"]
  COV-->CASE["Live insured case"]
  CASE-->CLAIM["Firm initiates claim with pack"]
  CLAIM-->ADJ["Adjudicate: up to 100k legal + 50k bond"]
  ADJ-->OVER{Bond over 50k?}
  OVER-->|yes|CLIENT["Client pays the overage"]
  OVER-->|no|DONE[Covered]
  

Provides

Policy number per insured subscriber; processes claims up to 100k legal + 50k bond (150k total — confirm per-incident vs aggregate).

Grace protects them

Claims blocked for 30 days (Profile Verification Window) — prevents day-one adverse selection.

Open / pinned

How the policy number flows back (API vs manual); who collects bond overage above 50k; required client fields; per-incident vs aggregate.

Partners

Sukoon (insurer/underwriter) + Howden (reinsurer). Member-facing copy stays generic; the names live in the admin underwriter config only.

Insurance eligibility & Family Shield

Priors gate insurance, and Family Shield resolves every insured before charging — so an ineligible person is excluded, never refunded. (Insurance state machine unchanged since v1 — verify against packages/kyc / entitlement code before a legal pass.)

flowchart TB
  START["Buyer picks a plan"]-->SOLO{Solo or Family Shield?}
  SOLO-->|solo|P1["Priors knockout (self)"]
  P1-->E1{Recorded prior?}
  E1-->|recorded|NOTE["Essential / notification-only
framed as a match, no insurance price"] E1-->|dismissed or clean|PAYS["Charge Shield at checkout"] SOLO-->|family|ROSTER["Roster: 2 adults + 2 dependants
Family Shield = flat 7,000, covers up to 4 eligible"] ROSTER-->MIN["Guardian declares priors for minors"] ROSTER-->SELF["Primary self-attests"] MIN-->PAYF["Charge flat 7,000 at checkout
(do not wait on a slow 2nd adult)"] SELF-->PAYF NOTE-->PAYE["Charge Essential"] PAYS-->ACT["Active (notification tier immediately)"] PAYF-->ACT PAYF-->|invite 2nd adult|ADULT2 PAYE-->ACTE["Active — notification-only
no insurance, no verification window"] ACT-->WIN["Profile Verification Window 30d
insurance attaches day 31 if verified + policy"] ADULT2["2nd adult self-attests later
via invite handshake (own PDPL consent)"]-->L{Eligible?} L-->|dismissed or clean|ACTV["Insurance activates for them
no extra charge (flat), no blocking"] L-->|recorded|NOTE2["Stays notification-only
flat price unchanged, nothing to refund"] ACTV-->WIN

Why no refunds

Eligibility resolves before the charge, and Family Shield is a flat 7,000 covering up to 4 eligible — a recorded-prior member is notification-only with no price change, so there's nothing to refund. No auth-hold; charge once at checkout.

PDPL (per-adult)

Each adult self-attests their own priors via an invite handshake with their own consent. The primary may declare for minors only.

Framing

Ineligible = routed to notification-only as a match, never a rejection. No "criminal / denied" wording on-screen.

Needs counsel

CBUAE product class (free-look), per-member activation, PDPL cross-border. No PCC for the dismissed exception.

Insurance status workflow

Insurance state is tracked per named member. The subscription can be active while a specific Family Shield member is pending, excluded, active, voided, or cancelled. (State machine unchanged since v1.)

flowchart TB
  START["Priors gate passed
insurance tiers selectable"]-->PAY["Charge at checkout
Shield 3,250 or Family 7,000"] PREKNOCKOUT["Recorded prior during onboarding"]-.->NA["not_applicable
Essential only; tiers hidden"] PAY-->SPLIT{Member path} SPLIT-->|solo Shield|SOLO["Primary member record"] SPLIT-->|Family primary or minor|FAM_READY["Named member present at checkout"] SPLIT-->|Family 2nd adult not ready|INVITE["Invite sent
adult self-attests later"] SOLO-->PROFILE{"Profile complete and clear?"} FAM_READY-->PROFILE INVITE-->PENDING["pending_profile_resolution"] PENDING-->ATTEST{"Adult self-attests"} ATTEST-->|recorded prior|EXCLUDED["excluded
notification-only; flat 7,000 unchanged"] ATTEST-->|clean or dismissed|PROFILE PROFILE-->|flagged / unclear / expired / missing consent|PENDING PROFILE-->|clear|WINDOW["pending_profile_window
30-day window"] WINDOW-->|day 31|POLICY["pending_policy_assignment"] POLICY-->|policy number assigned|ACTIVE["active
coverage attached"] POLICY-->|underwriting exclusion|EXCLUDED POLICY-->|admin failure|ESCALATE["Ops escalation"] ESCALATE-->POLICY ACTIVE-->FRAUD{"Fraud / misrepresentation?"} FRAUD-->|yes|VOIDED["voided
coverage never valid"] FRAUD-->|no|KEEP["No void action"] ACTIVE-->|refund / cancellation / renewal fail / removed|CANCELLED["cancelled
ended prospectively"]

Family Shield

excluded can apply to one additional adult while the 7,000 subscription stays active. Other eligible members continue.

Solo knockout

A solo user with a recorded prior never enters this workflow — Essential is the only selectable plan.

Voided vs cancelled

voided = fraud/misrepresentation. cancelled = prospective: refund, renewal failure, member removal.

ELA ops team (PH) & UAE supervisor

Always-on operators in the admin portal. They never block the happy path — STP auto-passes clean docs. They handle exceptions, monitor dispatch, run insurance setup, and are the direct handler when no firm is available.

flowchart TB
  subgraph KYC["KYC exceptions (async, never blocks STP)"]
    K1[Flagged doc arrives]-->K2["Three-pane: queue / viewer / decision"]
    K2-->K3{Decision}
    K3-->|approve|K4[Cleared]
    K3-->|resubmit|K5["Automated resubmit request (sheet)"]
  end
  subgraph DIS["Dispatch monitor (UAE 06:00-21:00)"]
    M1["Live SLA traffic-light countdowns"]-->M2{Breach / exhausted / none?}
    M2-->|breach|M3["Auto-escalate + Escalate-now"]
    M2-->|none available|M4["OPS-DIRECT: handle the case; can hand back to a firm"]
    M4-->M5["Member↔coordinator chat + ops phone"]
  end
  subgraph INSU["Insurance setup"]
    N1["Enter policy number: confirm-on-save + read-back"]-->N2["Starts verification window"]
    N3["Firm accepted + confirmed"]-->N4["Send insurance pack"]
  end
  subgraph AUD["Consulate audit"]
    A1["Append-only: email + time + message-id"]-->A2["Exportable evidence"]
  end
  K5 ~~~ M1
  M5 ~~~ N1
  N4 ~~~ A1
  

Sees

KYC three-pane viewer; dispatch timelines + traffic-light SLAs (dual UAE/PH clock); consulate audit (append-only, exportable); member 360; audit log; member↔coordinator case chat.

Does

Clear doc exceptions (never reject on OCR alone); monitor + escalate; handle ops-direct cases when no firm is available; enter policy number (starts grace); send insurance pack; maintain directories (firm hours, holidays).

Reviewer / canary sandbox

An allowlisted reviewer/canary phone (ELA_REVIEW_PHONE_ALLOWLIST) may open a case even under the production guard — force-routed through the safe sandbox outbox, so NO real firm/consulate is paged (the hourly dispatch canary uses this).

Hours — 24/7 vs desk

The system runs 24/7: arming instantly notifies firms, consulate, and contacts and starts location — no human needed. The desk is staffed UAE 06:00–21:00; only human actions (ops-direct takeover, doc review, insurance-pack send) wait for desk hours.

Edge cases & open gaps

The v1 P0/P1 cluster was resolved in the 2026-05-26 grill and much of it has since shipped. This tab tracks what's done and what's genuinely still open as of v2.

Shipped since v1

Ops-direct honesty rung. no_firm_available renders "Our team is handling this directly" — no fake firm search. Ops-direct is not a dead end (may resolve, or hand back to a firm).
Emirate firm matching. Firm pool filtered by residence emirate (location rung stubbed null; no geocoder). 7 per-emirate test firms live.
Watch Phase-2 arming. Wrist arms via phone-relay; independent direct path + fail-closed jti registry deployed (security-reviewed). WATCH-015 hardware evidence still pending.
Trusted contacts — no verification. Notify all entered at arm-time; founder accepts the mistyped-number leak.
Active-case = status timeline (Direction B), not chat-first; chat is one "Message us" tap + ops phone backup.
Realtime split-brain fix + durable case/session resilience (force-kill survival, cold-start restore).
Money path — Stripe/Tabby webhooks → own Postgres entitlement table (RevenueCat dropped); refund-tombstone gate closed.

Still open

LAUNCHComms are dormant. Firm/consulate/contact outbound runs in SIMULATE mode until provider creds (Twilio SMS/WA, email) + enable flags land + public Caddy carve-out for signed links. This is the top launch gate for the dispatch story.
OPSMonitoring not fully wired. Dead-man + dispatch-failure crons exist but need /etc/ela/monitoring.env (in progress); no error/telemetry sink yet (PostHog/Sentry undeployed — box RAM). See docs/strategy/self-validating-loop.md.
DRBackups — offsite + key custody. R2 offsite parked; backup + decryption-key hardening pending. Pre-launch blocker (details in internal ops notes).
PINInsurance-setup handoff — partner API vs manual, policy issuance + return of the policy number. Ops-mediated entry is the v1 placeholder.
BIZSingle-firm launch (1 firm/emirate; 3–5 hoped) · bond-overage collector (>50k) · VAT exemption on the insurance portion · ops vendor + test-recipient swap (prod ops details are currently the founder's).
WATCHWATCH-015 paired-hardware evidence (on-device receive/rotate/tombstone; direct-arm against a live registry). Independent arming is not yet phone-independent-verified on real hardware.
DECData residency framing — prod VPS is Linode London; the UAE-residency requirement was scrapped 2026-06-12 and S3 me-central-1 is pending-not-deployed. Member-facing residency copy needs a decision.