MVP Launch Checklist β phase-first organization (archived 2026-06-04)
ποΈ Archived 2026-06-04. This is the verbatim text of
docs/mvp-launch-checklist.mdimmediately before the 2026-06-04 structural re-org that switched the doc from phase-first (P0 β P1 β P2 as top-level sections) to domain-first (Onboarding / Channels / Billing / etc with priority as inline badges). Content was preserved verbatim across the re-org; this snapshot exists for posterity. For current state, seedocs/mvp-launch-checklist.md.
MVP Launch Checklist
Last updated: 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 Β§14 added for KB-via-Haystack (ADR-024 Accepted 2026-06-02, supersedes ADR-004 + ADR-009 RAGflow). 2026-06-01 baseline preserved below: Β§10 billing rail switched Orb β Lago β self-hosted (Railway for dev; AWS for staging + prod β see #1287) per #702 / #766 / #768; Β§12 Resend domain verification closed (#146 resolved 2026-06-01 by E); Β§13 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.
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 (Β§9, #701) Β· Lago billing (Β§10, #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).
This checklist defines what "launched" means for h.work MVP. Each item maps to a concrete outcome a real user will experience. When every P0 item is β , we're live.
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 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; Β§11 β landed via #696; full label-hygiene sweep β 95 open issues now satisfy exactly 1 priority + 1 type + 1 status), 2026-06-01 (Β§12 Resend β closed by E; Β§13 #677 β closed; Β§10 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 (Sections 1β8 carry over + Β§11 via #696 + Β§12 Resend β + Β§13 #677 β ) |
| β οΈ PARTIAL | 0 |
| β TODO | 13 (Section 9 ECS hosting + Section 10 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 Β§12.)
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.
π΄ P0 β Must Ship (Blocking Launch)β
1. Account Manager Can Set Up a Clientβ
The AM flow is the entry point for every client. Nothing else works without this.
- 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.
2. Client Can 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.
3. Client Can Send a Message and Get a Responseβ
The core loop. If this doesn't work, nothing matters.
- 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.
4. WhatsApp Channel Works End-to-Endβ
- Inbound webhook live β
+18566992888routes 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 +18566992888. Confirm message appears in conversation. Confirm reply arrives on WhatsApp.
5. Email Channel Works End-to-Endβ
- Cloudflare Email Routing configured β inbound emails to
bob@h852.work(staging) andbob@dev.h852.work(dev) forwarded to API webhook Cloudflare email worker deployed 2026-05-10: 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 (atlas@dev.h852.workβatlas@h852.work) applied for DB lookup. - Inbound webhook authenticated β HMAC verification via
CLOUDFLARE_EMAIL_WEBHOOK_SECRETImplemented atchannels.controller.ts:406β455: checksx-webhook-signature/x-cloudflare-signatureheader against HMAC-SHA256 of raw body. Worker shares the secret with Railway; HMAC verification active on dev + staging. - Email whitelist enforced β only whitelisted corporate domains can open conversations
resolveOrgFromEmailatchannels.controller.ts:417β482checks specialist (TO) first, then whitelist (FROM): exact email match (line 449), then domain match (line 464). Non-whitelist senders get auto-reply + drop (lines 571β591). Pre-normalization noisy check deferred until after mailparser extracts body so base64 raw bodies are not falsely classified noisy. - Resend outbound configured β
RESEND_API_KEYin Railway; sending domain verifiedemail.service.ts:29readsRESEND_API_KEYwith graceful no-op fallback. Outbound replies sent atchannels.controller.ts:647β661from specialist's routing address. Auto-replies use the verified email domain (notdev.*subdomain) so they don't bounce on unverified zones. - Per-OSA Gmail Send (ADR-0002 Phase 2) β backend complete via #1095 (2026-05-29); inbound dual-read primary path (#1093); refresh token encryption (#1065 / #1079). Resend remains the fallback for unprovisioned OSAs until #1062 column drop after soak.
- 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 Β§12. - 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.
6. Expert Workspace Worksβ
- 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.
7. Infrastructure Is Production-Readyβ
- 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'.
8. 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 Section 5). - 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'.
9. Production Hosting (ECS, not just Railway)β
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 Section 8).
- 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.
10. Lago Billing Live (no revenue without this)β
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.
-
LAGO_API_KEY(+LAGO_API_URLfor self-hosted) set in production ECS env - Lago customer + subscription created for Kaito's org on sign β wired into the AM flow that today calls Stripe
- Lago-backed invoice surfaces in
/client/settings/billingβ Kaito's admin can see the current invoice (Stripe mock pricing must not be visible) - Mid-month proration ledger (#514) β required because Specialist rates can change mid-cycle (rate edits need effective-date)
- 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.
11. Per-Org Sandboxed Agent Runtime (foundational)β
β 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.
- 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)
12. Resend / Email Sender Domain Verifiedβ
β Closed 2026-06-01 β domain verification resolved by E. #146 closed. Section 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)
13. 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.
14. KB via Haystack 2.x + Hayhooks (post-RAGflow)β
Added 2026-06-04. 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.
- 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.
π‘ P1 β Ship Before First Client (Week 1)β
AM Experienceβ
- 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)
Client Experienceβ
- 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)
Expert Experienceβ
- 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
WhatsAppβ
- Remaining numbers (
+19895822905,+19896934124) 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.
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 under the Channels P0 bucket now, not here. - Channel health dashboard visible to superadmin
π’ P2 β Post-Launch (Month 1)β
- Push notifications for expert escalations (E-P3)
- 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
- 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 in Β§10 via #702 / #766; supersedes #452 / #447; revenue structurally impossible until landed)
- Mid-month proration ledger (#514)
- Invoice history + download
- Business context upload during onboarding (KB ingestion)
- Microsoft Teams adapter unblocked
- Outbound org webhooks
- Per-org cost tracking
- Native mobile app for experts
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 | β Required | Pending #702 / #766; self-hosted Lago (Railway for dev; AWS for staging + prod β see #1287). No live billing integration yet β revenue structurally impossible until set. |
LAGO_API_URL | β Required | Self-hosted Lago endpoint (e.g. https://lago.h.work/api/v1). Pending #702. |
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.