MVP Launch Checklist
Last updated: 2026-06-04 — Structural re-org: domain-first, priority as inline badge (closes #1638). Content unchanged from the 2026-06-04 audit pass; only the grouping changed. Prior phase-first organization (P0/P1/P2 as top-level sections) is preserved verbatim in docs/_archive/mvp-launch-checklist-phase-first-2026-06-04.md. 2026-06-04 audit pass: §9 picked up promotion-flow CI guard (#1559), terraform-apply manual-only (#1577), and DEMO_MODE prod-guard CI check (#1510); §9 ECS cutover refs now also cite #1101 / #1303 / #1529; new RAG/KB section for Haystack (ADR-024 Accepted 2026-06-02, supersedes ADR-004 + ADR-009 RAGflow). 2026-06-01 baseline preserved: billing rail switched Orb → Lago — self-hosted (Railway for dev; AWS for staging + prod — see #1287) per #702 / #766 / #768; Resend domain verification closed (#146 resolved 2026-06-01 by E); identity bug closed (#677); priority shuffles per Eusden 2026-06-01 — Slack Official App submission (#1159) P1 → P0, Specialist prompt guardrails (#1194) P0 → P2, Veena mobile layout (#1210) P0 → P2.
Previously: 2026-05-25 — doc-hygiene pass: dropped obsolete WhatsApp-pool P1 item; added "Shipped since 2026-05-23" stanza below capturing #589, #621, #636, #642, #634, #623, #624, #604, #605, #600, #647.
✅ Audit reconciled 2026-05-23. The 2026-05-22 PM-persona audit (archive/MVP_CLIENT_CHANNELS_AUDIT_2026-05-22.md) flagged 8+ MVP-blockers. The headline three have shipped:
- #550 — outbound dispatch on Expert release for WhatsApp/Slack/Telegram → ✅ landed in PR #567 (
ChannelDispatcherService).- #551 — plaintext
integration_credentials→ ✅ landed in PR #570 (decrypt on every read site).- #554 — Resend dispatch no-retry → ✅ closed completed.
- #576 — auto-respond outbound dispatch (the
agent_replypath mirror of #550) → ✅ landed in PR #621.Remaining audit findings (hardcoded email subject, Expert workspace media rendering, channel badge gating, DB-backed DLQ) are tracked separately and are not launch-blocking under the revised launch channel scope below.
How this doc is organized
Each item carries an inline priority badge: 🔴 P0 (launch-blocking) · 🟡 P1 (ship before first client) · 🟢 P2 (post-launch month 1) · ⚪ P3 (later).
Items are grouped by domain, not by phase, so the question "what's launch criteria for billing?" (or channels, or onboarding) has one section to read. Each domain section starts with status + remaining P0 count so you can scan blockers at a glance.
Definition of launched lives at the bottom. Launch-day sequence + env-var audit also at the bottom.
Shipped since 2026-05-23 (doc refresh)
These landed on dev after the last full doc refresh and are reflected in the checkboxes below — listed here so readers don't have to diff git log against the doc date:
| PR | Landed | What |
|---|---|---|
| #589 | 2026-05-22 | #362 Phase B — per-Org sandboxed agent runtime: Railway provisioner with adapter pattern. Closes the last code-side P0 gap from the previous Implementation Status revision. ~25 follow-up fixes hardened idempotent teardown, Postgres advisory-lock serialization, agent-secret forwarding, and health-probe rotation. Currently in soak. |
| #621 | 2026-05-23 | Closes #576 — auto-respond replies now hit ChannelDispatcherService so the agent_reply outbound path mirrors the Expert-release path (#550 / #567). |
| #636 | 2026-05-24 | #631 — post-onboarding per-Org Specialist edit + rate-history audit table. |
| #642 | 2026-05-24 | Closes #556 — Telegram outbound media attachments (image/audio/file). |
| #634 | 2026-05-23 | Route refactor: /client/admin/specialists/* → /client/specialists/*. Old paths redirect. |
| #623 | 2026-05-23 | Client-side "Meet your Specialist" page wired to catalog profile. |
| #624 | 2026-05-23 | AM "New Client" wizard P0 design cleanup. |
| #604 | 2026-05-22 | AM "New Client" wizard: search Specialists by title. |
| #605 | 2026-05-22 | AM "New Client" wizard: UI consistency polish. |
| #600 | 2026-05-22 | Test-audit pass: fix 74 failing tests + add 33 hot-module unit tests. |
| #647 | 2026-05-24 | Canonical issue-label taxonomy (52→30 labels) + CI enforcement. |
Open P0 / mvp-blocker issues remaining (verified via GitHub API 2026-06-01): 0 with mvp-blocker. P0 punch-list under the locked channel scope is now: ECS migration (Infrastructure & Hosting, #701) · Lago billing (Billing & Revenue, #702 / #766 / #768) · AWS Secrets Manager (#830) · Kaito org provisioning (#1193) · Kaito KB seeding (#1195) · Specialist memory (#1197) · Pre-launch UAT (#1198) · Veena fast-path (#1208) · SLA breach alerting (#1209) · Official Slack App submission (#1159, promoted P1→P0 2026-06-01).
Launch-blocking channel scope (locked 2026-05-23)
Per product (E), MVP launches with these channels working inbound and outbound, text first:
| Channel | Tier | Why |
|---|---|---|
Webchat (/client/chat, authenticated client portal + subdomain routing) | P0 | Primary surface for client → Specialist + the only channel with Socket.io real-time. |
| WhatsApp (Twilio) | P0 | Most-demanded customer channel. |
| Email (Cloudflare → Resend) | P0 | Already fully working both ways; baseline. |
| Slack (events API) | P0 | Required for paid clients. Outbound dispatch landed in #567. |
| Telegram | P2 | Functional but not required to launch. Demoted from prior P1 framing. |
| Teams | P2 | Stub + credentials not configured (#122). Out of MVP scope. |
| P3 | XML parsing not implemented. |
Media support (image/audio/file outbound + client composer) is P1, not launch-blocking. See #559.
Audit Summary
Code audit completed 2026-05-04 against the original P0 sections 1–8; refreshed 2026-05-10 (onboarding/email/WA), 2026-05-22 (PM audit findings filed as #550, #551, #554, #576), 2026-05-23 (audit blockers landed), 2026-05-25 (Kaito-lens scrub: strict P0/P1 definitions; sections 9–13 added; per-Org runtime ✅ landed via #696; full label-hygiene sweep — 95 open issues now satisfy exactly 1 priority + 1 type + 1 status), **2026-06-01 (Resend ✅ closed by E; identity-attribution #677 ✅ closed; billing rail switched Orb → Lago (self-hosted; Railway for dev, AWS for staging + prod — see #1287) per #702 / #766 / #768; priority shuffles: #1159 P1→P0, #1194 P0→P2, #1210 P0→P2).
| Status | Count (of 65 P0 items) |
|---|---|
| ✅ DONE | 52 (Onboarding + Core loop + Channels + Expert + Agent runtime ✅ + Resend ✅ + identity ✅) |
| ⚠️ PARTIAL | 0 |
| ❌ TODO | 13 (Infrastructure & Hosting — ECS, plus Billing & Revenue — Lago) |
Cross-channel ship-blockers from the PM audit (separate from the 46 above): all 4 mvp-blocker GitHub issues closed completed as of 2026-05-23 (#550, #551, #554, #576). Telegram demoted from P1 to P2 — no longer launch-blocking.
2026-05-08 delta: Pre-launch QA revealed AM setup UX blockers (PR #263); prod build broken since May 1 due to duplicate DTO field (PR #265, now fixed). DEMO_MODE=false set in Railway.
2026-05-09: Production API deploying successfully. Agent service running. Full QA re-run in progress.
2026-05-10: Onboarding rebuilt as 3-step SPA (PRs #267–271). Email channel hardened end-to-end (Cloudflare worker → API routing for @dev.h852.work and @h852.work, subdomain TO normalization, threading fallback for replies without In-Reply-To). WhatsApp channel: heldForReview respected (no auto-reply when expert review pending), media/attachments passed through, 4096-char reply truncation, boot guard on missing TWILIO_AUTH_TOKEN. Billing page now shows trial state instead of "Free plan". Deactivated-org guard added on email + WhatsApp inbound paths. R2 buckets confirmed across all envs. Railway dev stale-build issue fixed via environmentTriggersDeploy mutation.
Remaining ops items (not code):
(All previously listed ops items are resolved as of 2026-06-01. DKIM/SPF for h.work verified in Resend — see Channels → Email below.)
Resolved 2026-05-10:
Onboarding multi-page wizard with channel/teammates/billing steps→ ✅ Rebuilt as 3-step SPA (Workspace Setup / Your h.work Team / Done) at single URL/onboarding/start?token=…. Email field disabled-input, inline[input].h852.workslug, flat sorted timezone list, AM email surfaced on team step. Continue →/client/chat(auto-authenticated). Idempotent re-visits supported (PRs #267–271).→ ✅ Allows re-fetch when invitationvalidateInvitationToken410s when org already activeaccepted; org/specialist/slug/profile fetches succeed for active orgs.Email worker stale subdomain routing→ ✅ Cloudflare worker deployed:@dev.h852.work→ dev API,@h852.work→ staging API. Worker sends base64 RFC822; API parses with mailparser. Dev specialist emails fixed (atlas|nova|bob@h852.work).Subdomain TO normalization→ ✅atlas@dev.h852.work→atlas@h852.workfor DB lookup; pre-normalization noisy check deferred until after mailparser extracts body.Auto-reply from unverified subdomain→ ✅ Auto-reply uses verified email domain, notdev.h852.work.Welcome email referenced removed steps→ ✅ "Invite your team" + "Set up billing" stripped from next-steps. Templates redesigned: no emojis, dark navy header, consistent CSS vars.Threading fails without In-Reply-To→ ✅ Fallback reuses most recent open conversation for sender+org pair.WhatsApp auto-replies even when expert is reviewing draft→ ✅heldForReviewrespected on email + WA paths.WhatsApp drops media attachments→ ✅ Media passed through tosendMessage; media-only inbound gets[Media message]placeholder.WA reply > 4096 chars rejected by Twilio→ ✅ Truncated client-side before send.Missing→ ✅ Startup boot guard logs warning.TWILIO_AUTH_TOKENsilently failsBilling page shows "Free plan" for trial orgs→ ✅ Now shows "Trial — N days remaining" / "Trial expired".Deactivated org email still creates conversations→ ✅ Returnsorg_inactive+ auto-reply "This workspace is no longer active". WhatsApp checks before creating any conversation.
Resolved since 2026-05-08:
→ ✅ Set toDEMO_MODE=truein productionfalsevia Railway API 2026-05-08Prod build broken (TS2300 duplicate avatarUrl)→ ✅ Fixed PR #265, deployed 2026-05-08T12:40 UTC→ ✅ Worker deployed; HMAC verification active on inbound webhook.CLOUDFLARE_EMAIL_WEBHOOK_SECRETnot set
Previously listed code blockers — all resolved in code:
WhatsApp→ ✅ Implemented with HMAC verify (/channels/whatsapp/statusmissingchannels.controller.ts:1217)Email inbound no HMAC→ ✅ Implemented viaCLOUDFLARE_EMAIL_WEBHOOK_SECRET(channels.controller.ts:406)Internal notes client-side only→ ✅ DB-level filter atconversations.service.ts:398Vercel hardcoded to staging API→ ✅vercel.jsonhas no hardcoded URLs; usesNEXT_PUBLIC_API_URLenv varJWT_SECRET dev fallbacks→ ✅ Boot guard inauth.module.ts; notifications also fixed
What We're Shipping
A named AI specialist (e.g. "Bob") sits inside a client's existing communication channels. Clients message Bob via WhatsApp, email, or the portal. Bob's AI agent drafts responses. Domain experts review, edit, and approve. Over time Bob gets smarter. Clients get work done without managing people.
Launch criteria by domain
Jump to: Onboarding · Core Conversation Loop · Channels · Expert Workflow · RAG / KB · Agent Runtime · Infrastructure & Hosting · Security & Secrets · Billing & Revenue
Onboarding
Status: ✅ Complete. AM → Client invite + onboarding wizard fully wired. Remaining P0: 0.
The Account Manager flow and client onboarding wizard are the entry point for every client. Nothing else works without these.
🔴 P0 — AM Setup
- AM can create a new org — company info, slug, domains, invite email
POST /orgsatorganizations.controller.ts:181(createAmOrg) accepts name/slug/industry/corporateDomains/adminEmail/timezone; UI atfrontend/src/app/ops/clients/new/page.tsx. Slug + corporate-domain validation present. - AM can assign a specialist — at least one specialist available in pool that is not already fully assigned; "Available" badge is truthful
POST /am/orgs/:orgId/specialistatorganizations.controller.ts:361; UI atfrontend/src/app/ops/clients/[id]/setup/step-2/page.tsx. Specialists returned withcurrentOrgAssignmentStatus("available"/"assigned") so the badge reflects actual assignment state. - AM can send client invite — email delivered with working OTP link (Resend configured, DKIM passing)
POST /am/orgs/:orgId/inviteatorganizations.controller.ts:266callsEmailService.sendOnboardingInviteEmail(email.service.ts:101) via Resend; 32-byte token + bcrypt-hashed 6-digit OTP. DKIM domain verification is operational config (verify in Resend dashboard). - Invite token expiry works — expired tokens show clear error, not crash
onboarding.service.ts:74validateToken throwsGoneException(HTTP 410) past 7d;confirmOtp:137throws Unauthorized past 10min OTP TTL;accept-invite/page.tsx:75catches 410 with friendly UI.
Verify: Create a brand-new org end-to-end in staging. AM → new org → assign specialist → send invite. Confirm email arrives.
🔴 P0 — Client Onboard
- OTP email arrives and works — client clicks link, enters code, sets password
accept-invite/page.tsxflows: validate token → enter 6-digit OTP →verifyInviteOtpissues JWT (lines 39–59); OTP delivered viaemail.service.ts:sendOtpEmail(line 443) using Resend. - Onboarding wizard completes — all 5 phases reachable without dead ends; no raw JSON errors shown
frontend/src/app/onboarding/step/[step]/page.tsxroutes steps 2–6 (slug, specialist, whitelist, invite teammates, complete); each backed by an endpoint inonboarding.controller.ts. Unknown-step fallback at lines 46–62 prevents dead ends. - [~] Slug is confirmed and live —
{slug}.h.workresolves correctly after phase 2.2 Backendslug-reservation.service.tsreserves transactionally and is immutable once set; CORS allows*.{HWORK_DOMAIN}(main.ts:40). Howeverfrontend/vercel.jsonhas no rewrites/domains config and Vercel project must be configured to attach the wildcard — DNS + Vercel domain attach not visible in repo. - Client lands in portal — correct redirect to
/client/chatafter onboarding completeAuthContext.tsx:getRoleRedirect(lines 72–79) returns/client/chatfor owner/admin/org_admin/customer;client/page.tsxredirects/portal→/client/chat. - Welcome email delivered — Resend sends onboarding confirmation
email.service.ts:sendWelcomeEmail(lines 245–355) called non-blocking fromonboarding.service.ts:completeOnboarding; also sendssendAmCompletionNotificationto assigned AM.
Verify: Full Phase 2 onboarding from invite link → wizard → portal landing. No broken steps.
🟡 P1 — AM polish
- Specialist availability correctly reflects assignment status (no misleading "Available" badge)
- AM can reassign specialist to a different org
- Raw JSON errors replaced with human-readable messages throughout AM flows
- Trial end date populated for all orgs (not just some)
🟡 P1 — Client polish
- Client can invite teammates during or after onboarding
- Client can see conversation volume chart (even if mocked)
- "Leave workspace" self-service works (issue #148)
- Accent color (
--accent) resolves to Claret#7E1D1Don deployed instances (currently showing blue)
🟢 P2 — Onboarding extensions
- Business context upload during onboarding (KB ingestion)
Core Conversation Loop
Status: ✅ Complete. Client → AI draft → (optional Expert review) → reply path is live end-to-end. Remaining P0: 0.
The core loop. If this doesn't work, nothing matters.
🔴 P0 — Send a Message and Get a Response
- Portal chat sends messages —
POST /conversations/:id/messagesreturns 200conversations.controller.ts:98–110returns 201; JWT-guarded; throttled 100 req/min/org. Frontend callssendMessage()infrontend/src/lib/api.ts:395. - AI agent responds — OpenRouter/OpenAI configured; agent generates a reply
conversations.service.ts:470–499callsagentClient.chat()overAGENT_SERVICE_URL;OPENAI_BASE_URL=https://openrouter.ai/api/v1documented in.env.example:168. Fail-open stub returns flagForReview=true if agent unreachable. - Response appears in UI — polling or SSE delivers agent message to client without page refresh
Socket.io
agent_message_sentemitted innotifications.gateway.ts:172–187; consumed byuseNotificationshook (line 128) with 8s polling fallback (line 30). - CORS fixed for Vercel preview deployments — PR #244: dynamic
*-humanityprotocol.vercel.appURLs now allowed in non-production (was causing blanket 500s) - [~] Message attributed correctly — client sees "Bob" (specialist persona), not agent internals
UI resolves specialist firstName from
conversation.specialistId(PortalChat.tsx:527, 560–562) with fallback toconversation.agentName. Note:message.roleenum is"user"|"agent"|"expert"— there's nospecialistIddenormalised on the message itself, so attribution depends on conversation-level lookup. - [~] AI vs expert label visible — client can see whether response was AI-generated or expert-reviewed
Distinguished by
message.role("agent" vs "expert"); no explicitisAiGenerated/reviewedByExpertflag.riskLevelis stored on Message andconfidence/flagForReviewin metadata, but the client UI surfaces the role-based label only — verify it actually renders an "AI" vs "Expert" pill inPortalChat. - Prompt injection false-positives fixed — PR #244: Bob no longer rejects greetings like "Hi Bob" as prompt injection; specialist persona header injection also fixed
Verify: Log in as client, open new thread, send message, confirm reply arrives within 30s.
✅ Closed — Identity & attribution bugs (Kaito loop integrity)
✅ Closed 2026-06-01 — #677 is closed. No outstanding launch-blocking identity/attribution bugs at this time. New regressions get filed under the standard bug flow.
- #677 — Expert queue shows client as WhatsApp display name "E" instead of "Amy from Acme"; Specialist mis-attributed. Closed.
Channels
Status: ✅ Code complete on all P0 channels. Outbound media is P1, not launch-blocking. Remaining P0: 0 in code. Operational gates (Meta WABA limit increase, Slack official-app submission #1159) tracked under Infrastructure & Hosting / Slack respectively.
Channel scope locked 2026-05-23 (see table at top): Webchat + WhatsApp + Email + Slack are P0; Telegram is P2; Teams/WeChat are out of MVP scope.
🔴 P0 — WhatsApp End-to-End
- Inbound webhook live —
+185****2888routes tohttps://api-staging-bd3f.up.railway.app/channels/whatsapp/inboundchannels.controller.ts:732@Post("whatsapp/inbound")routes viarouteTwilioWhatsApp(). Twilio number→Railway URL mapping is operational config (verify in Twilio console). - Sender whitelist enforced — whitelisted numbers get routed; unknown senders get auto-reply
channels.controller.ts:796–811queriesWhatsAppSenderWhitelist; on miss, sends rate-limited auto-reply via Redis (24h gate,whatsapp.service.ts:300–308). Admin UI atwhatsapp-org.controller.ts:26–60. - TWILIO_SUBACCOUNT_TOKEN set in Railway — outbound sends don't silently fail
Confirmed set on Railway envs 2026-05-10. Boot guard added: API logs a startup warning if
TWILIO_AUTH_TOKENis missing so silent-fail mode is observable. Code reads it correctly atwhatsapp.service.ts:265–273for sends andchannels.controller.ts:741for HMAC verify. - Outbound reply delivered — agent response is sent back via WhatsApp to the sender
whatsapp.service.ts:260–290sendMessage()posts to Twilio with auth;channels.controller.ts:831–835invokes after agent reply. - Status callback working — delivery receipts hitting
/channels/whatsapp/statusHandler implemented atchannels.controller.ts:1217with HMAC-SHA1 Twilio signature verification, structured logging, and audit trail. - Message deduplication working — duplicate Twilio webhooks don't create duplicate messages (Redis dedup key)
channels.controller.ts:765–776MessageSid dedup with 1h TTL viagetRedisClient().
Verify: Send WhatsApp message from a whitelisted number to +185****2888. Confirm message appears in conversation. Confirm reply arrives on WhatsApp.
🔴 P0 — Email End-to-End
2026-06-05 architecture update (PR #1722, closes #1060): Gmail API polling + DWD-impersonated outbound is now the primary Specialist email path. The Cloudflare Worker + Resend outbound flow is retained as a fallback during a 2-week prod soak (decommission tracked in #1440 / #1441). Resend is otherwise reserved for invites/onboarding/transactional via
email/email.service.ts.
- Gmail API inbound polling (PRIMARY) — Gmail API polling via cron (
*/30 * * * * *on Railway dev) reads each Specialist mailbox using ONE Google Workspace service account with domain-wide delegation. Shipped 2026-06-05 via #1722. - Gmail API outbound via DWD (PRIMARY) — Outbound replies sent from the Specialist's gsuite mailbox via DWD impersonation; no per-Specialist OAuth tokens. Specialist mailboxes provisioned automatically by
organizations.service.tsenqueuingWORKSPACE_JOB_CREATEon Specialist↔Org assignment; processor calls Admin SDKusers.insert. Shipped via #1722. - MX records → Google —
h.work,h852.work,h853.workall MX-routed to Google Workspace; inbound delivery for the Gmail path is live. - 2-week prod soak in progress — Cloudflare Worker fallback retained until #1440 / #1441 close. Verify Gmail polling latency + RFC 2822 Message-ID / historyId observability follow-ups (#2064, #2065).
- Cloudflare Email Routing (FALLBACK) — Cloudflare email worker deployed 2026-05-10 still routes
@dev.h852.work→ dev API,@h852.work→ staging API. Worker sends base64 RFC822; API parses with mailparser atchannels.controller.ts:303@Post("email/inbound"). Subdomain TO normalization applied for DB lookup. Decommission after soak. - Inbound webhook authenticated (FALLBACK path) — HMAC verification via
CLOUDFLARE_EMAIL_WEBHOOK_SECRETatchannels.controller.ts:406–455against HMAC-SHA256 of raw body. - Email whitelist enforced — only whitelisted corporate domains can open conversations
resolveOrgFromEmailatchannels.controller.ts:417–482checks specialist (TO) first, then whitelist (FROM): exact email match, then domain match. Non-whitelist senders get auto-reply + drop. - Resend transactional outbound configured —
RESEND_API_KEYin Railway; sending domain verifiedemail.service.ts:29readsRESEND_API_KEYwith graceful no-op fallback. Resend is no longer the Specialist conversation channel — reserved for invites/onboarding/transactional email and auto-replies on the fallback path. - Per-OSA Gmail Send (ADR-0002 Phase 2) — superseded by DWD service-account model — Phase 2 originally shipped via #1095 (2026-05-29); inbound dual-read primary path (#1093); refresh token encryption (#1065 / #1079). Per-OSA OAuth tokens were replaced 2026-06-05 by the single Google Workspace service account + DWD model (#1722).
- DKIM passing — outbound emails from
@h.workdon't land in spam Resolved 2026-06-01 by E. DKIM/SPF/return-path verified in Resend dashboard. See "Resend domain verification" closed-section below. - Threading works — reply-to-email stays in same conversation thread (In-Reply-To header)
Inbound parses In-Reply-To at
channels.controller.ts:329–344; routing maps it toexternalThreadId(lines 603–610). Outbound preserves In-Reply-To + References (lines 697–700) and strips "Re:" from subject (line 621). Threading fallback (PR 2026-05-10): replies without In-Reply-To reuse the most recent open conversation for the sender+org pair. -
heldForReviewrespected on auto-replies — when agent draft is held for expert review, no auto-reply is sent Verified on both email and WhatsApp paths 2026-05-10. - Deactivated-org guard — emails to a deactivated org return
org_inactiveand auto-reply "This workspace is no longer active" Email and WhatsApp both checkOrganization.status === 'deactivated'before creating any conversation.
Verify: Send email to bob@[org-slug].h.work. Confirm it creates a conversation. Reply from portal. Confirm reply arrives as email in correct thread.
✅ Closed — Resend / Email Sender Domain Verified
✅ Closed 2026-06-01 — domain verification resolved by E. #146 closed. Retained for historical context; no remaining launch gates.
-
h.workverified in Resend (DKIM, SPF, return-path) — resolved 2026-06-01 -
onboarding@h.workis the From: header on invite + OTP + welcome emails (nothptestingsite.comordev.h852.work) - DKIM passes on inbox-providers test (Gmail, Outlook, ProtonMail)
🟡 P1 — WhatsApp ops
- Remaining numbers (
+198****2905,+198****4124) registered and ONLINE after Meta WABA limit increase approved - Display name "Bob [h.work]" approved by Meta and visible to recipients
-
Pool routing works when sender messages different pool numbers across sessions— dropped 2026-05-25: pool model superseded by single-number-per-env routing in PR #213 (2026-05-03); item is obsolete under current architecture.
🟡 P1 — Other channels
-
Slack adapter unblocked — credentials configured for at least one test workspace— promoted to P0 (2026-06-01) via #1159 Official Slack App submission; tracked in this section's P0 column once submitted. - Channel health dashboard visible to superadmin
🟢 P2 — Channel extensions
- Microsoft Teams adapter unblocked
Expert Workflow
Status: ✅ Code complete. Expert queue, draft edit/send/resolve, internal notes, and per-Org filtering all live. Remaining P0: 0.
🔴 P0 — Expert Workspace
- Expert can log in —
expert1@humanwork.dev(or real expert) reaches/workspace/queueAuthContext.tsx:74mapsexpert→/workspace/queue. Seed userdavid.kim@h852.work(Expert) — seeapi/src/database/seed.tsandapi/src/auth/auth.service.ts:267for the canonical demo-login mapping. - Queue shows inbound items — messages from clients appear with risk level and AI draft
expert-queue.controller.ts:35–51GET /expert-queue; UI atfrontend/src/app/workspace/queue/page.tsx:122–148renders risk +item.message?.confidence. - Expert can edit and send — edit AI draft, click send; message delivered to client
POST /expert-queue/:id/respond(expert-queue.controller.ts:74–89); persists withrole='expert'(expert-queue.service.ts:115–126). Edit mode inExpertWorkspacePane.tsx:79–128. - Expert can write from scratch — clear draft, write manually, send
Edit mode lets expert blank
editTextand submit a fresh response (ExpertWorkspacePane.tsx:116–128); fallback CTA "Compose a fresh response in the Client View" at line 146. - Expert can resolve ticket — ticket moves out of queue on resolve
POST /expert-queue/:id/resolve(expert-queue.controller.ts:91–104); setsstatus='resolved'+resolvedAt/resolvedBy(expert-queue.service.ts:99–150). - Internal notes invisible to client — notes marked
is_internalfiltered at DB levelconversations.service.ts:386–399checks caller role; non-internal roles getandWhere('msg.is_internal = false')at query level, not just UI. - Ticket assigned correctly — conversation stays with the right specialist/expert after assignment
expert-queue.service.ts:listPending(lines 50–87) joinsorg_experts.orgIdso experts only see queues for orgs they're assigned to.
Verify: As expert, pick up a queued item, edit the draft, send, resolve. Confirm client sees the message.
🟡 P1 — Expert polish
- Expert profile page exists — name, bio, skill tags
- Queue auto-refresh works reliably (no stale items)
- Resolved tab shows resolved items correctly
- Analytics page loads without errors
🟢 P2 — Expert extensions
- Push notifications for expert escalations (E-P3)
- Native mobile app for experts
RAG / KB
Status: Haystack 2.x + Hayhooks is the canonical RAG runtime (ADR-024 Accepted 2026-06-02). Backend live in dev + staging + prod; one env-var hygiene item remains. Remaining P0: 1 (RAGFLOW_* env-var purge).
Added 2026-06-04 from the prior phase-first §14. ADR-024 was Accepted 2026-06-02; Haystack/Hayhooks is the canonical RAG runtime for h.work, superseding ADR-004 and ADR-009 (RAGflow). Implementation propagated across docs in PR #1455. Tracked under #1280 / haystack-deploy runbook.
🔴 P0 — Haystack canonical RAG
- ADR-024 Accepted — Haystack 2.x + Hayhooks is the live RAG runtime (Railway for dev; AWS ECS Fargate for staging + prod via
humanwork-ragCloud Map service); all live docs reference Haystack, RAGflow callouts retained only as tombstones (seedocs/api/reference.md:246-250). - KB search endpoint for Expert workspace — PR #1526 (backend) + #1528 (UI panel).
- Production Haystack/Hayhooks deployment — staging + prod run on AWS ECS Fargate as the
humanwork-ragservice (#1527 Terraform; #1541rag-deploy.yml). Dev remains on Railway viarag/railway.toml. -
RAGFLOW_*env vars purged from prod — onlyHAYSTACK_*/HAYHOOKS_*remain; legacy compatibility tombstones documented.
Agent Runtime
Status: ✅ Per-Org sandboxed agent runtime landed 2026-05-25 via #696 (RIG runtime hard-cut). 2026-06-03 update: Per-org Railway runtime kind (
railway_per_org) retired via ADR-028 + #1584 / #1450. Supported runtime kinds are nowshared_railway(default) andecs_per_org(premium). ECS prod cutover tracked in #701. One nightly smoke-test soak still TBD but not launch-blocking. Remaining P0: 0.
✅ Landed 2026-05-25 via PR #696 (RIG runtime hard-cut). #362 closed. Manifest-backed per-org runners (
runner_manifests/runner_registrations), sandbox workers, no shared ambient runtime, no broad runner vendor secrets. 1:1 Org ↔ runtime supported; the Org×Specialist v2 path (#401) is structurally enabled via optionalspecialist_idon manifests/registrations/runs.
🔴 P0 — Per-Org Sandboxed Agent Runtime
- 1:1 Org ↔ Agent runtime container — landed in #696
- Per-Org OpenAI key / tool-permissions scope — landed in #696
- Routing in
ConversationsService.callAgent()picks the right runtime byorg_id— landed in #696 - Cross-org isolation enforcement (DB + runtime) — ADR-020 + Org×Specialist classification shipped 2026-05-29 via PR-A..E (#1148–#1153);
expert_agent_messagesdropped (PR-E #1153); audit fields populated by every write site (#1185 / #1187 closed remaining gaps 2026-05-31). - Smoke: cross-org isolation test passes nightly (#237 — tracked under #947 isolation epic; code-side enforcement landed via #981 + #1024, nightly soak still TBD)
Infrastructure & Hosting
Status: ❌ Production cutover to ECS is the largest remaining P0 block. Railway is fine for dev; staging boot validated; prod bootstrap, secrets manager, webhook repointing all still TODO. Tracked in #701. Remaining P0: 7 (ECS provisioning + deploy + secrets + DB + webhook repointing × 3).
🔴 P0 — Production hosting baseline (Railway today)
- Railway production env deployed —
api-production-f95e.up.railway.appserving latestmainbranchrailway.jsonconfigures DOCKERFILE builder +/healthhealthcheck. Confirmation that the URL serves latestmainis operational, not visible in repo. - Vercel frontend deployed — env vars correct
frontend/vercel.jsonhas no hardcoded API URLs.api.ts:6usesNEXT_PUBLIC_API_URL ?? 'http://localhost:3000'. Production URL set via Vercel dashboard env vars (see.env.production.example). - All required env vars set in production — no
undefinedconfigs causing silent failures (see env var audit below).env.exampleis comprehensive but actual Railway production values are not visible in repo. Per the audit table at the bottom,DEMO_MODEis currently true and must be flipped;STRIPE_SECRET_KEY/SLACK_SIGNING_SECRETneed verification. - Database migrations run — no pending migrations on production DB
38 migrations under
api/migrations/;data-source.ts:67setssynchronize: falseandmigrationsRun: false(explicit, manual).app.module.ts:133–135keeps synchronize off in production. Operational confirmation needed thatnpm run typeorm:runwas executed against prod DB. - Redis connected — session dedup, rate limiting, auto-reply gating all functional
api/src/common/redis.util.ts:getRedisClient()builds an ioredis client fromREDIS_URL; returns null when unset and callers degrade gracefully. - RLS enforced — no cross-org data leakage (verified by attempting cross-org API calls)
OrgRlsInterceptorregistered globally atapp.module.ts:180viaAPP_INTERCEPTOR; setsapp.current_org_idper request. Note that DB-level RLS policies are still partial (per CLAUDE.md, full DB RLS tracked in issue #3); application-layer enforcement is in place. - DEMO_MODE=false in production — confirmed flipped in Railway 2026-05-08
Code is correctly gated:
auth.controller.ts:84–93blocks/auth/demo-loginwhenNODE_ENV==='production'ORDEMO_MODE!=='true'.
🔴 P0 — Production Hosting on AWS ECS (cutover)
Added 2026-05-25 (Kaito-lens). Railway is fine for dev; staging + prod target AWS ECS Fargate. Terraform + GitHub Actions landed #1101; staging boot validated #1303; prod bootstrap #1529; CI promotion enforcement #1559; manual
terraform-apply#1577;rag-deployworkflow #1541. Tracked in #701 (owner: Emanuele).
- ECS infrastructure provisioned for h.work — same pattern Emanuele built for HP recruiter; spin-up in flight as of 2026-05-25 sync. Staging/prod cutover tracked under #1101 / #1303 / #1529.
- API + Agent + Frontend deployed to ECS — not Railway — for the production environment Kaito will use.
- Production secrets in AWS Secrets Manager — not plain Railway env vars (also see Security & Secrets).
- Production database (RDS) migrated and reachable from ECS —
synchronize: false, migrations run, backups configured. - Twilio webhook base URL repointed to the ECS production URL.
- Cloudflare email worker repointed to ECS production.
- Slack events URL repointed to ECS production.
- Promotion flow CI-enforced (
dev → staging → main) — landed via PR #1559 on 2026-06-03; theguardworkflow is a required status check that rejects PRs tomainnot originating fromstaging(and tostagingnot fromdev). Seedocs/promotion-flow.md. Mandatory once ECS prod is live so emergency hotfixes can't bypass review. -
terraform-applyis manual-only (workflow_dispatch) — landed via PR #1577; no infra change applies on push. Required for the ECS cutover so a runaway PR can't reshape the prod cluster. -
DEMO_MODEproduction guards + CI check — PR #1510 added a CI guard + docs that fail-closed preventDEMO_MODE=truefrom leaking into a production deploy. Pairs with the env-var audit row below; supersedes the 2026-05-08 one-off Railway flip.
Verify: Send a WhatsApp from Kaito's number to the production WhatsApp number → message appears in Veena's queue on the production frontend → Veena replies → Kaito gets the reply, all on ECS, with no Railway URL in the chain.
Security & Secrets
Status: ⚠️ Two operational items remain (Twilio token rotation, per-env MASTER_ENCRYPTION_KEY check). HMAC verification fully wired on every P0 webhook. Remaining P0: 2 (Twilio rotate + MASTER_ENCRYPTION_KEY per-env).
🔴 P0 — Critical Security
- Twilio main auth token rotated — old token (
fc59a4...) invalidated; shared in plaintext in chat Token doesn't appear hardcoded anywhere in repo (grepfc59a4returns only this file). Rotation is a Twilio console action — confirm out-of-band that the leaked token was invalidated and Railway has the new value. - [~] HMAC verification on all webhooks — WhatsApp, Slack, email all reject unsigned requests
Slack:
verifySlackSignature(channels.controller.ts:261–287) HMAC-SHA256 + timing-safe compare + 5min replay window. WhatsApp: TwiliovalidateRequest(). Telegram: HMAC-SHA256. Email inbound has none (see Channels → Email). - JWT secret is production-strength — boot guard in place
auth.module.ts:15–28throws at startup ifNODE_ENV==='production'andJWT_SECRETis unset, shorter than 32 chars, or starts withdev-.notifications.module.tsandnotifications.gateway.tsuse no fallback (cfg.get('JWT_SECRET')directly). - MASTER_ENCRYPTION_KEY is unique per env — staging and production keys differ
common/crypto.ts:14–23validates 32-byte length but doesn't enforce per-env uniqueness. Verify out-of-band that staging and production Railway projects have distinct values; falls back to unencrypted mode silently if absent. - No seed credentials accessible in production — demo login endpoints return 403 in prod
auth.controller.ts:84–93double-gates demo-login onNODE_ENVandDEMO_MODE. Seed runner short-circuits in prod atseed.service.ts:70–71(APP_ENV==='production'returns). - CORS locked down —
ALLOWED_ORIGINSonly includes production domainsmain.ts:42–56enforces explicit origin allowlist +*.{HWORK_DOMAIN}regex; rejects unknown origins with explicit error. Demo Vercel origins only allowed whenDEMO_MODE==='true'.
Billing & Revenue
Status: 🟡 Lago integration partially complete — secrets confirmed, LagoProvider active. Immediate gap:
specialist_monthlyplan bootstrap (PR #2933) plus Kaito sync. Tracked in #702 (owner: Alexander K). Remaining P0: 4.
Promoted to P0 on 2026-05-25 (Kaito-lens). 2026-05-29 rail switch: Orb → Lago (self-hosted, open-source — Railway for dev; AWS for staging + prod (AWS bring-up tracked in #1287)) —
LagoProvideradapter on PR #1069; docs propagated in PR #1071. Stripe is the legacy direct path; post-cutover Stripe lives only inside Lago's PSP config. Kaito is a paying client — without Lago live we cannot invoice them, which means we cannot have a paying client, which means we are not launched. Tracked in #702 and #766 (owner: Alexander K). Supersedes #452.
🟡 P0 — Lago Billing Live
Status as of 2026-06-19 (Pablo investigation): LagoProvider is confirmed active on staging —
ops/billing/customerreturns real Lago UUIDs (created Jun 10).LAGO_API_URL+LAGO_API_KEYconfirmed in AWS Secrets Manager for staging + prod. ECS task wired correctly. Immediate bootstrap gap:specialist_monthlyplan was never created in Lago → all subscriptions havelagoSubscriptionId=null. Runlago:bootstrap(PR #2933), then sync Kaito. Mid-month proration ledger remains P2 for MVP because Kaito launches on a fixed monthly Specialist rate; promote #514 back to P0 if mid-cycle rate edits become launch scope.
-
LAGO_API_KEY(+LAGO_API_URLfor self-hosted) set in production ECS env (confirmed in AWS Secrets Manager, 2026-06-19) - LagoProvider active (real Lago customer UUIDs returned from staging
/ops/billing/customer, not MockProvider) -
specialist_monthlyplan created in Lago and Kaito subscription synced — runlago:bootstrapvia PR #2933 workflow for staging then prod, then triggerops/billing/sync-customer/:orgIdfor Kaito - Lago-backed invoice surfaces in
/client/settings/billing— Kaito's admin can see the current invoice - Legacy Stripe / Request Finance / plan-tier teardown (#768) — remove deprecated endpoints, DB columns, seed data, UI before cutover
- Smoke: end-to-end invoice for one full billing cycle in staging Lago sandbox before pointing Kaito at prod
Verify: AM creates Kaito's org → Lago shows subscription created → invoice line items appear in Kaito's billing tab → first invoice cycle closes correctly in the Lago sandbox.
🟢 P2 — Billing extensions
- Specialist monthly rate visible to client at onboarding Step 2 (PR #518 closes #498)
- Lago invoicing pipeline live before first paying client (now tracked as P0 above via #702 / #766; supersedes #452 / #447; revenue structurally impossible until landed)
- Mid-month proration ledger (#514)
- Invoice history + download
- Per-org cost tracking
Other P2 / P3 (post-launch)
Items that don't fit a single launch domain and aren't blocking the first paying client.
- Client approval UI for high-stakes agentic actions (C-W8) — demoted to P2 (2026-05-23). For MVP we ask clients for approval manually via the existing conversation channel; the dedicated UI is post-launch.
- SLA performance dashboard for clients
- Outbound org webhooks
Env Var Audit (Production Must-Haves)
These must be set and correct in Railway production before launch:
| Variable | Status | Notes |
|---|---|---|
DATABASE_URL | ✅ Set | |
JWT_SECRET | ✅ Set | Verify it's not the staging value |
MASTER_ENCRYPTION_KEY | ✅ Set | Must differ from staging |
OPENAI_API_KEY / OPENROUTER_API_KEY | ✅ Set | Verify balance / rate limits |
RESEND_API_KEY | ✅ Set | Verify sending domain verified |
REDIS_URL | ✅ Set | |
TWILIO_ACCOUNT_SID | ✅ Set | |
TWILIO_API_KEY + TWILIO_API_SECRET | ✅ Set | Per-env key SK79b70e... |
TWILIO_SUBACCOUNT_SID | ✅ Set | ACfc8480... |
TWILIO_SUBACCOUNT_TOKEN | ✅ Set | Verify not rotated |
TWILIO_WEBHOOK_BASE_URL | ✅ Set | https://api-production-f95e.up.railway.app |
STRIPE_SECRET_KEY | 🗑️ Legacy | Direct Stripe is deprecated; post-cutover Stripe lives only inside Lago's PSP config. Removal tracked in #768. |
LAGO_API_KEY | ✅ Set (staging + prod) | In AWS Secrets Manager for staging + prod (confirmed Emanuele, 2026-06-19). dev uses Railway Lago. Remaining: run lago:bootstrap on staging/prod + verify BillingModule off MockProvider (#702). |
LAGO_API_URL | ✅ Set (staging + prod) | In AWS Secrets Manager for staging + prod (confirmed Emanuele, 2026-06-19). dev uses Railway Lago endpoint. |
SLACK_SIGNING_SECRET | ⚠️ Check | Optional outside prod (env fallback). In production each Slack-connected org provides its own signing_secret via integration_credentials per ADR-002 — env-var fallback gated to non-prod (#1219, 2026-06-01). BLK-004 unblocked. |
DEMO_MODE | ✅ false | Set 2026-05-08; verified post-deploy |
NODE_ENV | ✅ Should be production | |
ALLOWED_ORIGINS | ✅ Set | Verify only prod domains |
HWORK_DOMAIN | ✅ Set | h852.work (staging) / h.work (prod) |
CLOUDFLARE_EMAIL_WEBHOOK_SECRET | ✅ Set | Shared with Cloudflare email worker; HMAC verify active |
R2_BUCKET_NAME | ✅ Set | hwork-dev / hwork-staging / hwork (prod) — all confirmed exist 2026-05-10 |
R2_ACCOUNT_ID | ✅ Set | Confirmed across dev/staging/prod Railway envs |
R2_ACCESS_KEY_ID | ✅ Set | Confirmed across dev/staging/prod Railway envs |
Launch Day Sequence
- T-48h — Full staging smoke test: AM setup → client onboard → WhatsApp message → expert queue → resolve
- T-24h — Production env var audit; rotate any test secrets; confirm DEMO_MODE=false
- T-12h — Run DB migrations on production; verify no pending
- T-4h — Deploy
mainto Railway production + Vercel; smoke test production URLs - T-1h — Send test WhatsApp to production number; confirm end-to-end
- T-0 — First real client invite sent by AM
Definition of Done
MVP is launched when:
- An Account Manager can set up a real client org in production
- That client receives an invite, onboards, and sends their first message via WhatsApp or portal
- An expert sees it in their queue, reviews the AI draft, and sends a response
- The client receives the response
Everything else is iteration.