MAYDAY · MVP IMPLEMENTATION

Implementation, API & Admin Console

Siri-triggered personal-safety iOS app. Mocked Twilio integration. Server-side primitives ready for an animated incident dashboard and multi-user admin console. Single page, all the visuals inline.

Trigger Hey Siri → Mayday Target iOS 17+ Twilio mocked (primitives only) Auth NextAuth.js (Google + phone) DB self-hosted Postgres Admin view-only · secure host Version 2026-06-27 v2
1. One-pager 2. Architecture 3. API primitives 4. iPhone trigger flow 5. Animated dashboard 6. Admin console 7. Data model 8. Twilio mock layer 9. Stack 10. 14-wk plan 11. Risks 12. UC coverage 13. Open decisions 14. Audit trail

1 · One-pager

Mayday is an iPhone personal-safety app that turns "Hey Siri, Mayday" into a full incident: camera burst + SMS to contacts + outbound call to an AI agent + GPS pings for 10 min. The MVP mocks Twilio entirely — backend records the intent to call/text/SMS so we can build the incident pipeline, the dashboard, and the admin console against real data without paying per-minute.

14 wks
from green light → TestFlight beta

Timeline

5 phases, 2-3 weeks each, each ending with a verifiable demo. No "research sprints".

4 devs
iOS · backend · web · SRE/QA

Team

2 senior + 2 mid. Mobile is the riskiest hire; everything else has a clear playbook.

$2.8k / mo
at MVP scale · 10k MAU

Burn

No Twilio/voice costs in MVP — everything flows through the mock layer. Real voice tier deferred to Phase 4.

What this page proves: (1) the data flow from iPhone trigger → server → admin console is concrete and shippable, (2) the Twilio mock captures every primitive the real Twilio SDK will need, (3) the admin UX shows what multi-tenant ops look like before we pay for any external service.

2 · Runtime architecture

iPhone fires an App Intent → phone posts to the relay → relay fans out the four primitives (record, sms, call, locate) → all events stream to a WebSocket that both the per-user dashboard and the admin console consume.

iPHONE Siri: "Mayday" AppIntent perform() AVCaptureSession MFMessageUI CallKit bridge CLLocationManager multipart upload REST + WebSocket RELAY API Fastify · Node 20 · Fly.io POST /v1/incidents POST /v1/incidents/:id/events GET /v1/incidents/:id (stream) POST /v1/contacts/notify POST /v1/voice/initiate Drizzle ORM · Postgres + Redis pub/sub for streams TWILIO MOCK local queue · scripted delays sms.send(number, body) voice.dial(to, onAnswer) recording.url(callSid) scripted call tree status webhooks (delay 200-2k ms) swap-in: twilio-node v5 swap-in: el v2 voices USER DASHBOARD Live Activity + Web real-time incident view countdown to next ping cancel via admin (Phase 4) evidence locker ADMIN CONSOLE multi-user · staff-only live incident feed map view · audit log manual intervention search · export HTTPS fan-out WSS WSS
Mock swap-in: the relay API talks to a TwilioAdapter interface. The mock implementation is the default in MVP; production simply sets TWILIO_ADAPTER=real and injects the SDK. Same call sites, same response shapes, same webhook signatures.

3 · API primitives (mocked Twilio)

Every endpoint the iPhone app and the admin console consume. Authentication is short-lived JWT signed with HS256, scoped per role (user / admin / agent).

3.1 · Mobile → Relay

POST /v1/incidents                        # create incident, returns {id, started_at}
POST /v1/incidents/:id/events             # batched: gps_ping, sensor, app_state
POST /v1/incidents/:id/media              # multipart upload, 30s clip chunks
GET  /v1/incidents/:id                    # full record incl. contact status
POST /v1/incidents/:id/cancel             # user aborts; requires reason
POST /v1/incidents/:id/contacts/notify    # server-side SMS fan-out (mocked)
POST /v1/incidents/:id/voice/initiate     # server-side call to contact (mocked)
WS   /v1/incidents/:id/stream             # real-time event stream

3.2 · Twilio mock → Relay (webhooks, scripted)

POST /v1/webhooks/twilio/sms/status       # queued · sent · delivered · failed
POST /v1/webhooks/twilio/voice/status     # ringing · answered · completed
POST /v1/webhooks/twilio/voice/recording  # recording_url · duration · transcript
POST /v1/webhooks/twilio/voice/dtmf       # user pressed 1/2/3 in call tree

3.3 · Admin → Relay

GET  /v1/admin/incidents                  # filter by status, user, region
GET  /v1/admin/incidents/:id              # full timeline + media
POST /v1/admin/incidents/:id/intervene    # take over, push 911, dispatch field
GET  /v1/admin/users                      # all registered + last active
GET  /v1/admin/audit                      # operator actions · exportable

3.4 · Response shapes (canonical)

// Incident — server-authoritative
{
  "id":          "inc_01HXY...",
  "user_id":     "usr_8x9k",
  "status":      "active" | "resolved" | "cancelled" | "failed",
  "started_at":  "2026-06-27T17:42:11Z",
  "ended_at":    null,
  "last_ping":   { "lat": 37.4220, "lng": -122.084, "t": "..." },
  "media":       [{ "kind": "video", "url": "r2://...", "duration_s": 30 }],
  "contacts":    [{ "name":"Sarah", "phone":"+1...", "status":"answered", "ts":"..." }],
  "voice":       { "call_sid":"CAxxx", "status":"in_call", "recording":null }
}

// Event — append-only audit row
{
  "id":         "evt_01HXY...",
  "incident_id":"inc_01HXY...",
  "kind":       "trigger" | "gps_ping" | "media_uploaded" | "contact_sms_sent" |
                "contact_call_initiated" | "contact_call_answered" | "voice_dtmf" |
                "user_cancelled" | "admin_cancelled" | "admin_intervened",
  "at":         "2026-06-27T17:42:13Z",
  "data":       { ... kind-specific ... }
}
Idempotency: every POST accepts an Idempotency-Key header. Mobile retries on flaky network; the mock layer replays the same scripted sequence if the key matches. This is the discipline that makes the mock indistinguishable from real Twilio.

4 · iPhone trigger flow — what the user sees

The mockup below shows the iPhone screen the moment after "Hey Siri, Mayday" fires. The relay has already accepted the incident; the iPhone is now streaming events back as the mock layer "dials" and "sends".

5:42 PM
Mayday Active 00:08
GPS Lock
±4 m
37.4220° N · 122.0841° W
Camera
REC · 00:08 / 30s
front + back · 1080p · uploading
Contacts notified
S Sarah K. SMS SENT
M Marcus L. CALLING
J Jamie P. ANSWERED
D 911 (RapaidSOS) QUEUED
Dispatching · cannot cancel

Mock incident · synthetic data

What fires when

t (ms)ActorAction
0SiriRecognizes "Mayday" → invokes TriggerEmergencyIntent
+80iPhonePOST /v1/incidents with device GPS, contacts list, battery level
+150iPhoneAVCaptureSession starts dual-cam 30s capture to local buffer
+200RelayPersists incident row + opens WS stream
+220MockEnqueues 4 SMS jobs (scripted delay 200ms-2s each)
+250MockEnqueues voice.dial to first contact (scripted ring-time 4-8s)
+400iPhoneOpens WS to relay, displays Active screen
+800iPhoneStarts GPS ping every 5s; uploads first 5s video chunk
+5,000MockContact #1 SMS delivered; webhook received
+8,000MockContact #2 voice "answered"; DTMF handler engaged
+10,000AdminConsole shows new incident row; live updates begin
Cancel posture (Phase 4): no user-visible cancel in MVP per coercion-resistance discussion (§13.2). kill_all() on the adapter is reserved for admin console intervention only. The kill contract still matters: if an admin pulls the plug, every in-flight SMS halts and every active call hangs up. Idempotent, auditable, signed.

5 · Animated incident dashboard (per-user)

The user's own incident view — same data the admin sees, but scoped to their incidents only. Mockup below is a static frame of the live view; in production this is a WS-driven React/Next.js page with the same colour grammar.

Status
● ACTIVE
Since 17:42:11 · 8s elapsed
Next GPS ping
02.7 s
auto every 5s · jitter ±0.4s
Media uploaded
12 MB
2 chunks · 60s remaining
SF · Mission · 17:42
trail · last 90s
at
event
contact
channel
status
17:42:11.080
trigger
siri
17:42:11.220
contact_notify
Sarah K.
sms
sent
17:42:11.250
voice_dial
Marcus L.
call
ringing
17:42:13.110
media_chunk
r2
5s · 6MB
17:42:15.700
voice_answer
Marcus L.
call
answered
17:42:18.220
gps_ping
core
±4m
Animation source: in production the dashboard is a Next.js page hooked to useIncidentStream(id) which subscribes to the WS endpoint. State updates are diff'd, not full re-renders. Below is the same UI in test mode — the admin can scrub through a recorded incident frame-by-frame.

6 · Admin console — multi-user ops

Staff console for ops, support, and trust & safety. Real users (us, beta cohort) appear as rows in the feed; the map shows cluster density. This page is the first thing we'll show investors when we demo the "ops layer" — it proves we're not just building an app, we're building a service.

mayday-admin · ops.mayday.dev v1.0.0 · last 24h · auto-refresh 5s
Active now
3 +1
Last 24h
17
False positives
2
Median time-to-first-contact
8.2s
Active incidents
incident
user
started
contacts
state
inc_01HX…
Morsy C. · SF
17:42:11
4 / 4 sent
● active
inc_01HX…
Sarah K. · Oakland
17:38:54
3 / 5 sent
● active
inc_01HW…
Jamie P. · Berkeley
17:21:02
2 / 2 sent
○ sms-failed
Cluster
Bay Area · 4 active
Operator actions today
17:42
intervene · inc_01HX… (manual 911 push)
ops-3
17:31
export · inc_01HW… (police request)
ops-1
Recently resolved
incident
user
duration
contacts
outcome
inc_01HV…
Marcus L. · Palo Alto
02:18
4 / 4 sent · 2 ans
✓ resolved (safe)
inc_01HU…
Riley N. · SF
00:42
3 / 3 sent · 1 ans
✓ resolved (admin)
inc_01HT…
Avery W. · SF
08:11
5 / 5 sent · 1 ans
✓ resolved (911 dispatched)
Roles: ops can intervene · support read-only on incidents · admin full audit access. Every operator action writes to the audit log with the operator id, the affected user, and a reason field that's required for sensitive actions (intervene, export, terminate).

7 · Data model (Drizzle / Postgres)

Three tables that survive every state of the system. The events table is append-only — that single discipline lets us replay any incident deterministically.

// users — minimal PII; identity managed by Clerk/Auth0 in production
users (
  id          text primary key,
  phone       text unique not null,
  name        text,
  created_at  timestamptz default now(),
  last_seen_at timestamptz,
  contacts    jsonb -- [{name, phone, priority}]
)

// incidents — one row per trigger; mutable status
incidents (
  id          text primary key,
  user_id     text references users(id),
  status      text not null -- active | resolved | cancelled | failed,
  started_at  timestamptz not null,
  ended_at    timestamptz,
  last_ping   jsonb,
  contacts_notified jsonb -- [{phone, status, ts}],
  voice       jsonb -- {call_sid, status, recording_url},
  cancel_reason text
)
create index on incidents (status, started_at desc);
create index on incidents (user_id, started_at desc);

// events — append-only audit; INSERT-only, never UPDATE
events (
  id          bigserial primary key,
  incident_id text references incidents(id),
  kind        text not null,  -- trigger | gps_ping | media_uploaded | contact_sms_sent |
                       contact_call_initiated | contact_call_answered | voice_dtmf |
                       user_cancelled | admin_cancelled | admin_intervened
  at          timestamptz default now(),
  data        jsonb
)
create index on events (incident_id, at);
-- Append-only enforced via revoke update/delete on app role

7.1 · Media storage

Video clips and call recordings go to Cloudflare R2 with server-side encryption (AES-256). URLs are short-lived signed (5 min) so the admin console can render evidence without exposing permanent links. Retention: 90 days for user-initiated, 365 days for incidents involving 911 escalation.

8 · Twilio mock layer — primitives + behaviour

The mock is a real, scriptable component — not a stub. It implements the full surface Twilio exposes and replays deterministic sequences so tests can assert exact state.

Adapter interface

interface TwilioAdapter {
  sms: {
    send(to: string, body: string):
      Promise<{sid: string, status: 'queued' | 'sent' | 'delivered' | 'failed'}>
  }
  voice: {
    dial(to: string, onAnswer: (sid: string) => void):
      Promise<{sid: string, status: 'queued' | 'ringing' | 'answered' | 'completed'}>
    hangup(sid: string): Promise<void>
    recording(sid: string):
      Promise<{url: string, duration: number, transcript?: string}>
  }
  kill_all(): Promise<{killed: number}>  // cancel path
}

Scripted scenarios (mock)

ScenarioSequence
happy_pathSMS 200ms · call rings 4s · answers · DTMF 1 safe
voicemailSMS 200ms · call rings 22s · voicemail
no_answerSMS 200ms · call rings 30s · no-answer · next contact
network_failSMS fails 3× · call fails · marked failed
user_cancelany state · drain queue · hangup all
Determinism matters: each scenario seeds the mock's RNG so tests can assert exact webhook order. Without this, "the integration test passed locally but failed in CI" becomes the dominant bug class.

8.1 · Swap-in checklist (when Twilio turns on)

  1. Set TWILIO_ADAPTER=real in env
  2. Replace mock URLs with real Twilio webhooks; verify HMAC signatures match
  3. Verify each of the 5 scripted scenarios still produces the same event sequence (allow wall-clock jitter ±2s)
  4. Confirm kill_all() maps to Twilio Calls.update({status:'completed'}) for all in-flight
  5. Re-run load test at 50 concurrent incidents · verify no missed webhooks

9 · Tech stack

Mobile (iOS)

Swift 5.10 SwiftUI App Intents ActivityKit CallKit AVFoundation WatchConnectivity

Backend

Node 20 Fastify 5 TypeScript strict Drizzle ORM Postgres 16 Redis 7 (pub/sub) BullMQ

Web / Admin

Next.js 15 React 19 TanStack Query Zustand Tailwind v4 shadcn/ui MapLibre GL

Data / infra

Fly.io (API + WS) Neon (Postgres) Upstash (Redis) Cloudflare R2 (media) Cloudflare Pages (web) Sentry Grafana Cloud

External (Phase 4)

Twilio Voice Twilio SMS ElevenLabs v2 OpenAI Realtime RapidSOS Ready Clerk (auth)

Tooling

pnpm + Turborepo ESLint + Prettier Vitest Playwright Detox (iOS e2e) GitHub Actions

9.1 · Env vars (server refuses to start if missing)

NODE_ENV              production | staging | dev
PORT                  8080
DATABASE_URL          postgres://...
REDIS_URL             redis://...
R2_BUCKET             mayday-evidence
R2_ACCESS_KEY         ...
R2_SECRET_KEY         ...
WS_AUTH_SECRET        # HS256 JWT
TWILIO_ADAPTER        mock | real    # default mock in MVP
TWILIO_ACCOUNT_SID    # required only if TWILIO_ADAPTER=real
TWILIO_AUTH_TOKEN     ...
TWILIO_FROM_NUMBER    +1...
SENTRY_DSN            ...
ADMIN_JWT_SECRET      ...

10 · 14-week sprint plan

WEEK 1-2Foundations · mocked

Monorepo · API scaffold · Twilio mock

1.
Monorepo — pnpm + Turborepo, apps/api, web, mobile, mock.
pnpm-workspace.yaml, turbo.json, packages/shared-types
2.
API scaffold — Fastify + Drizzle + healthz + request-id middleware.
apps/api/src/server.ts, apps/api/src/db/schema.ts
3.
TwilioAdapter interface + MockTwilioAdapter with all 5 scenarios.
packages/twilio-adapter/{index,mock,real}.ts
4.
Incident endpoints — POST/GET, idempotency-key, JWT auth.
apps/api/src/routes/incidents.ts
5.
Events table + append-only DB role.
apps/api/src/db/migrations/0001_init.sql

🚦 Gate 1 — Mock end-to-end

curl POST /v1/incidents → adapter queues SMS + voice → webhooks fire → events table populated. Tested with 3 scenarios.

WEEK 3-4iOS · Siri trigger

App Intents · camera burst · incident UI

1.
App Intents frameworkTriggerEmergencyIntent + AppShortcutPhrase("Mayday").
apps/mobile/Sources/Intents/TriggerEmergency.swift
2.
AVCaptureSession multi-cam 30s pipeline to local buffer + chunked upload.
apps/mobile/Sources/Capture/MultiCamRecorder.swift
3.
Incident UI — Active screen (no cancel — irreversible state per §13.2), contact status list.
apps/mobile/Sources/Views/IncidentActiveView.swift
4.
WS client with reconnect, exponential backoff.
apps/mobile/Sources/Network/IncidentStream.swift
5.
ActivityKit Live Activity showing incident state on Lock Screen.

🚦 Gate 2 — Siri fires end-to-end

"Hey Siri Mayday" on TestFlight build → camera records → API receives → mock fires webhooks → Live Activity updates. Manual test on 3 devices.

WEEK 5-6User dashboard · animated

Per-user Next.js dashboard · WS-driven

1.
Next.js app — incident detail page, hook to WS endpoint.
2.
Animated dashboard — KPI cards, live feed table, GPS map.
apps/web/app/incident/[id]/page.tsx
3.
Test-mode scrubber — replay recorded incidents for QA.
4.
Evidence locker — list media chunks, signed R2 URLs.

🚦 Gate 3 — Dashboard visible to user

Trigger an incident on iPhone → user dashboard shows same view as admin in real time, ≤500ms latency.

WEEK 7-8Admin console

Multi-user ops console · roles · audit

1.
Admin routes/v1/admin/incidents, /intervene, /audit.
2.
Console UI — sidebar, KPI row, live feed, cluster map, recent resolved.
3.
Role middlewareops / support / admin with reason field on sensitive actions.
4.
Export — incident JSON export (police request workflow).

🚦 Gate 4 — Admin can intervene

Trigger incident as user → admin sees in feed → admin hits "intervene" → all in-flight mocks drain (kill_all). Audit row written.

WEEK 9-10Watch + Action Button

Apple Watch app · Action Button integration

1.
WatchOS app — double-crown press triggers iOS intent via WatchConnectivity.
2.
Action Button — register TriggerEmergencyIntent as system action in Settings.
3.
NegativeAppShortcutPhrases — train Siri to ignore "mayday" in conversation context.
4.
Crash detectionCMMotionActivityManager triggers intent on high-G event.

🚦 Gate 5 — Three trigger surfaces

Siri, Action Button, Watch crown, and crash detection all fire the same intent. Manual test on iPhone 16 Pro + Watch Ultra 2.

WEEK 11-12Hardening

Latency · tests · observability

1.
Latency budget audit — measure t=0 → first contact SMS at p50/p95.
2.
Load test — 50 concurrent incidents, verify no missed webhooks.
3.
e2e suite — Playwright (web) + Detox (iOS) covering happy path + 4 failure modes.
4.
Grafana dashboards — incident rate, false-positive rate, time-to-first-contact, admin actions.

🚦 Gate 6 — Production-ready binary

Load test green · all e2e green · dashboards live · p95 time-to-first-contact < 9s.

WEEK 13-14Beta + launch

TestFlight · safety narrative · App Store

1.
TestFlight 50 users — friendlies, family, 2 rideshare-driver beta cohorts.
2.
Safety narrative doc for App Review (privacy nutrition labels, mic/camera purpose strings).
3.
Privacy policy + data retention policy + on-screen indicators.
4.
App Store submission with safety category.

🚦 Gate 7 — Beta live

50 users actively using the app · 0 SEV1 incidents in mock layer · App Review first-pass response received.

11 · Engineering risks & mitigations

RiskLikelihoodImpactMitigation
App Store rejection on safety/911/camera groundsHighCriticalPre-submission call with Apple review board · safety narrative doc · on-screen indicators · privacy nutrition labels accurate
False-positive Siri triggers burning trustMediumHighNegativeAppShortcutPhrase set · admin console can resolve-but-not-cancel after the fact · log every dispatch + every reason
Mock/real drift — what works in mock breaks in prodHighHighAdapter contract tests run against both adapters · swap-in checklist (Phase 4) · load test at 50 concurrent on real before turning on
Webhook loss — Twilio never confirms a statusMediumHighIdempotency keys · reconcile job every 5min queries Twilio for in-flight SIDs · alert if > 5% unconfirmed
Camera + upload battery drain drains user's phone mid-incidentMediumHighStream at 720p default · chunk size 5MB · upload pauses when battery < 20% · show battery indicator in Active UI
iOS background-mic denial if we ever try to add itLowCriticalDon't · Siri trigger is the only path · documented as design principle
Single engineer ships safety-critical bug at 11pmMediumCritical2-reviewer rule on safety paths · CI blocks merges without approval · on-call rotation starts Wk 8
Provider model change (Twilio pricing, ElevenLabs API)MediumMediumAdapter abstraction · monthly cost review · Gemini Live as backup · 2-week swap budget per provider

12 · Use-case coverage (audit vs. usecases.html)

Every UC in usecases.html mapped to the implementation section that covers it. MVP scope is what ships in the first TestFlight build; deferred are explicit post-MVP per the original UC scoping.

Pre-event

UCNameImplementationStatus
UC-1DiscoverApp Store listing + landing page (§9 stack)MVP
UC-2InstallTestFlight + App Store (Wk 13-14)MVP
UC-3OnboardNextAuth Google + phone OTP · 5-screen flow (§3.4 + §10 Wk 1)MVP
UC-4DrillDrill button → triggers mock incident end-to-end (§10 Wk 2)MVP
UC-5Shift-armApp in foreground (deliberate arm) — no background daemon in MVPdeferred

The event

UCNameImplementationStatus
UC-6Trigger"Hey Siri Mayday" → AppShortcutPhrase (§4 + §10 Wk 1)MVP
UC-7CaptureAVCaptureSession multi-cam 30s → R2 (§10 Wk 2)MVP
UC-8BroadcastTwilio mock: SMS to 5 contacts with live link (§3.2 + §8)MVP
UC-9VoiceMock call + scripted AI tree (§8 scenarios) — real AI voice Phase 4mocked
UC-10DispatchAdmin console "intervene" button (§6) — RapidSOS mockedmocked
UC-11ResolveSafe-word flow (§10 Wk 6) — admin marks resolvedMVP

Post-event

UCNameImplementationStatus
UC-12EvidenceAdmin export · signed R2 URLs (§6)MVP
UC-13AftercareCounseling referral email — partner org listdeferred

Coverage summary

BucketTotal UCsMVP-readyMockedDeferred
Pre-event5401
The event6420
Post-event2101
Total13922

9 UCs ship in first TestFlight · 2 mocked (UC-9 voice, UC-10 dispatch) · 2 deferred (UC-5 background arm, UC-13 aftercare partner integration).

UC-5 (background arm) & UC-6 (trigger from any state): because we removed background listening, UC-5 is "app foreground" only. UC-6 still works in any state if the user has Siri enabled — that's Apple's contract. We document this trade-off in onboarding.

13 · Open decisions & locked items

13.1 · Locked (your call this turn)

DecisionChoiceImplication
Beachhead userRideshare drivers (Maria persona)Use cases & GTM unchanged; consent flow stays phone-first
Twilio go-liveMocked through Phase 4 (real later)TwilioAdapter interface ships in Wk 1; TWILIO_ADAPTER=mock default
911 providerMocked through Phase 4Admin "intervene" = simulated dispatch · real RapidSOS wired in Phase 4
AuthNextAuth.js — Google + phone OTP providersReplaces custom JWT — saves ~1 wk; use-case UC-3 unchanged
DB hostingSelf-hosted Postgres — own box (no Fly.io)Reduces dependency surface; ops cost is your time not $$
Apple accountReuse existing organisation accountTestFlight external testing unlocked from Wk 7
Admin console authNone for MVP — runs on secure hostView-only dashboard · no public exposure

13.2 · Cancel posture — design decision

Your point about coercion is sharp and the literature backs it: most safety apps have a "cancel within X seconds" because false positives are catastrophic for trust. But the same UI is a vulnerability under threat. Resolution we agreed on: no user-visible cancel in MVP; admin console is the off-switch.

PhaseCancel behaviorReasoning
MVP (TestFlight beta) — what we shipNo cancel on iPhone UI · admin can resolve-but-not-cancel · incident is irreversible from user sideCoercion resistance is non-negotiable for a safety app — once dispatched, only an operator pulls the plug. Beta cohort = friendly users, false positives get classified post-hoc.
Phase 4 (real-world users)Same posture, but admin dashboard gets a single "Cancel incident" button with reason field · signed audit rowAdmin (or pre-authorised trusted contact via phone tree) is the off-switch. Even emergency-stop has accountability.
V2Optional "duress PIN" — entering PIN silently escalates to police instead of cancellingStandard duress pattern · Morsy's mention of PIN aligns with this. PIN entry looks like cancel but actually triggers a hidden second alert.
Recommendation: ship MVP as designed (no cancel). Instrument every dispatch + admin action so we have data to confirm the posture at Wk 8 review. Re-evaluate only if beta signal shows accidental-fire rate > 5%.

13.3 · Still pending (non-blocking)

  1. Domain + SSL for admin consoleops.mayday.dev on a Cloudflare proxy in front of your box?
  2. iOS minimum version — iOS 17+ confirmed (App Intents + negative phrases require 17). Confirm we drop iOS 16 support?
  3. Beachhead city — SF / Atlanta / NYC for first 50-user TestFlight?
  4. Drill cadence — UC-4 says drill in onboarding only. Should it be monthly push-notification prompt?
Default picks if you don't want to decide today: ops.mayday.dev on your box · iOS 17 minimum · SF first · monthly drill prompt. Reply "use defaults" and I'll lock them in.

14 · Audit trail

#Command / actionResult
1skill_view strategy-html-artifactloaded — confirms 3-file trilogy pattern
2skill_view cloudflare-pages-deployloaded — wrangler 4.105 works non-interactively
3wrangler pages project listconfirmed mayday-impl not in list — new project
4curl POST api.cloudflare.com/.../pages/projects name=mayday-implproject created · subdomain mayday-impl.pages.dev
5write_file /Users/morsy/dev/mayday-impl/index.htmlv1 · 64,939 bytes · dark theme · 13 sections
6wrangler pages deploy via mktemp -d stagingsuccess · 1.62s · URL https://ed8341a0.mayday-impl.pages.dev
7curl -L GET verifyHTTP 200 · 64,939 bytes · text/html — live
8curl usecases.html → extract 13 UCs (UC-1 → UC-13)use-case coverage matrix built · 9 MVP · 2 mocked · 2 deferred
9patch index.html × 3 (meta chips, TOC, coverage + locked + cancel sections)v2 ready · 7 locked decisions + cancel posture table
10wrangler pages deploy v2 (next step)pending