Skip to main content

MVP Launch Checklist β€” phase-first organization (archived 2026-06-04)

πŸ—„οΈ Archived 2026-06-04. This is the verbatim text of docs/mvp-launch-checklist.md immediately 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, see docs/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_reply path 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:

PRLandedWhat
#5892026-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.
#6212026-05-23Closes #576 β€” auto-respond replies now hit ChannelDispatcherService so the agent_reply outbound path mirrors the Expert-release path (#550 / #567).
#6362026-05-24#631 β€” post-onboarding per-Org Specialist edit + rate-history audit table.
#6422026-05-24Closes #556 β€” Telegram outbound media attachments (image/audio/file).
#6342026-05-23Route refactor: /client/admin/specialists/* β†’ /client/specialists/*. Old paths redirect.
#6232026-05-23Client-side "Meet your Specialist" page wired to catalog profile.
#6242026-05-23AM "New Client" wizard P0 design cleanup.
#6042026-05-22AM "New Client" wizard: search Specialists by title.
#6052026-05-22AM "New Client" wizard: UI consistency polish.
#6002026-05-22Test-audit pass: fix 74 failing tests + add 33 hot-module unit tests.
#6472026-05-24Canonical 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:

ChannelTierWhy
Webchat (/client/chat, authenticated client portal + subdomain routing)P0Primary surface for client β†’ Specialist + the only channel with Socket.io real-time.
WhatsApp (Twilio)P0Most-demanded customer channel.
Email (Cloudflare β†’ Resend)P0Already fully working both ways; baseline.
Slack (events API)P0Required for paid clients. Outbound dispatch landed in #567.
TelegramP2Functional but not required to launch. Demoted from prior P1 framing.
TeamsP2Stub + credentials not configured (#122). Out of MVP scope.
WeChatP3XML 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).

StatusCount (of 65 P0 items)
βœ… DONE52 (Sections 1–8 carry over + Β§11 via #696 + Β§12 Resend βœ… + Β§13 #677 βœ…)
⚠️ PARTIAL0
❌ TODO13 (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.work slug, flat sorted timezone list, AM email surfaced on team step. Continue β†’ /client/chat (auto-authenticated). Idempotent re-visits supported (PRs #267–271).
  • validateInvitationToken 410s when org already active β†’ βœ… Allows re-fetch when invitation accepted; 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.work for DB lookup; pre-normalization noisy check deferred until after mailparser extracts body.
  • Auto-reply from unverified subdomain β†’ βœ… Auto-reply uses verified email domain, not dev.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 β†’ βœ… heldForReview respected on email + WA paths.
  • WhatsApp drops media attachments β†’ βœ… Media passed through to sendMessage; media-only inbound gets [Media message] placeholder.
  • WA reply > 4096 chars rejected by Twilio β†’ βœ… Truncated client-side before send.
  • Missing TWILIO_AUTH_TOKEN silently fails β†’ βœ… Startup boot guard logs warning.
  • Billing page shows "Free plan" for trial orgs β†’ βœ… Now shows "Trial β€” N days remaining" / "Trial expired".
  • Deactivated org email still creates conversations β†’ βœ… Returns org_inactive + auto-reply "This workspace is no longer active". WhatsApp checks before creating any conversation.

Resolved since 2026-05-08:

  • DEMO_MODE=true in production β†’ βœ… Set to false via Railway API 2026-05-08
  • Prod build broken (TS2300 duplicate avatarUrl) β†’ βœ… Fixed PR #265, deployed 2026-05-08T12:40 UTC
  • CLOUDFLARE_EMAIL_WEBHOOK_SECRET not set β†’ βœ… Worker deployed; HMAC verification active on inbound webhook.

Previously listed code blockers β€” all resolved in code:

  • WhatsApp /channels/whatsapp/status missing β†’ βœ… Implemented with HMAC verify (channels.controller.ts:1217)
  • Email inbound no HMAC β†’ βœ… Implemented via CLOUDFLARE_EMAIL_WEBHOOK_SECRET (channels.controller.ts:406)
  • Internal notes client-side only β†’ βœ… DB-level filter at conversations.service.ts:398
  • Vercel hardcoded to staging API β†’ βœ… vercel.json has no hardcoded URLs; uses NEXT_PUBLIC_API_URL env var
  • JWT_SECRET dev fallbacks β†’ βœ… Boot guard in auth.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 /orgs at organizations.controller.ts:181 (createAmOrg) accepts name/slug/industry/corporateDomains/adminEmail/timezone; UI at frontend/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/specialist at organizations.controller.ts:361; UI at frontend/src/app/ops/clients/[id]/setup/step-2/page.tsx. Specialists returned with currentOrgAssignmentStatus ("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/invite at organizations.controller.ts:266 calls EmailService.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:74 validateToken throws GoneException (HTTP 410) past 7d; confirmOtp:137 throws Unauthorized past 10min OTP TTL; accept-invite/page.tsx:75 catches 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.tsx flows: validate token β†’ enter 6-digit OTP β†’ verifyInviteOtp issues JWT (lines 39–59); OTP delivered via email.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.tsx routes steps 2–6 (slug, specialist, whitelist, invite teammates, complete); each backed by an endpoint in onboarding.controller.ts. Unknown-step fallback at lines 46–62 prevents dead ends.
  • [~] Slug is confirmed and live β€” {slug}.h.work resolves correctly after phase 2.2 Backend slug-reservation.service.ts reserves transactionally and is immutable once set; CORS allows *.{HWORK_DOMAIN} (main.ts:40). However frontend/vercel.json has 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/chat after onboarding complete AuthContext.tsx:getRoleRedirect (lines 72–79) returns /client/chat for owner/admin/org_admin/customer; client/page.tsx redirects /portal β†’ /client/chat.
  • Welcome email delivered β€” Resend sends onboarding confirmation email.service.ts:sendWelcomeEmail (lines 245–355) called non-blocking from onboarding.service.ts:completeOnboarding; also sends sendAmCompletionNotification to 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/messages returns 200 conversations.controller.ts:98–110 returns 201; JWT-guarded; throttled 100 req/min/org. Frontend calls sendMessage() in frontend/src/lib/api.ts:395.
  • AI agent responds β€” OpenRouter/OpenAI configured; agent generates a reply conversations.service.ts:470–499 calls agentClient.chat() over AGENT_SERVICE_URL; OPENAI_BASE_URL=https://openrouter.ai/api/v1 documented 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_sent emitted in notifications.gateway.ts:172–187; consumed by useNotifications hook (line 128) with 8s polling fallback (line 30).
  • CORS fixed for Vercel preview deployments β€” PR #244: dynamic *-humanityprotocol.vercel.app URLs 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 to conversation.agentName. Note: message.role enum is "user"|"agent"|"expert" β€” there's no specialistId denormalised 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 explicit isAiGenerated / reviewedByExpert flag. riskLevel is stored on Message and confidence/flagForReview in metadata, but the client UI surfaces the role-based label only β€” verify it actually renders an "AI" vs "Expert" pill in PortalChat.
  • 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 β€” +18566992888 routes to https://api-staging-bd3f.up.railway.app/channels/whatsapp/inbound channels.controller.ts:732 @Post("whatsapp/inbound") routes via routeTwilioWhatsApp(). 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–811 queries WhatsAppSenderWhitelist; on miss, sends rate-limited auto-reply via Redis (24h gate, whatsapp.service.ts:300–308). Admin UI at whatsapp-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_TOKEN is missing so silent-fail mode is observable. Code reads it correctly at whatsapp.service.ts:265–273 for sends and channels.controller.ts:741 for HMAC verify.
  • Outbound reply delivered β€” agent response is sent back via WhatsApp to the sender whatsapp.service.ts:260–290 sendMessage() posts to Twilio with auth; channels.controller.ts:831–835 invokes after agent reply.
  • Status callback working β€” delivery receipts hitting /channels/whatsapp/status Handler implemented at channels.controller.ts:1217 with 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–776 MessageSid dedup with 1h TTL via getRedisClient().

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) and bob@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 at channels.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_SECRET Implemented at channels.controller.ts:406–455: checks x-webhook-signature / x-cloudflare-signature header 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 resolveOrgFromEmail at channels.controller.ts:417–482 checks 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_KEY in Railway; sending domain verified email.service.ts:29 reads RESEND_API_KEY with graceful no-op fallback. Outbound replies sent at channels.controller.ts:647–661 from specialist's routing address. Auto-replies use the verified email domain (not dev.* 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.work don'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 to externalThreadId (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.
  • heldForReview respected 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_inactive and auto-reply "This workspace is no longer active" Email and WhatsApp both check Organization.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/queue AuthContext.tsx:74 maps expert β†’ /workspace/queue. Seed user david.kim@h852.work (Expert) β€” see api/src/database/seed.ts and api/src/auth/auth.service.ts:267 for the canonical demo-login mapping.
  • Queue shows inbound items β€” messages from clients appear with risk level and AI draft expert-queue.controller.ts:35–51 GET /expert-queue; UI at frontend/src/app/workspace/queue/page.tsx:122–148 renders 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 with role='expert' (expert-queue.service.ts:115–126). Edit mode in ExpertWorkspacePane.tsx:79–128.
  • Expert can write from scratch β€” clear draft, write manually, send Edit mode lets expert blank editText and 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); sets status='resolved' + resolvedAt/resolvedBy (expert-queue.service.ts:99–150).
  • Internal notes invisible to client β€” notes marked is_internal filtered at DB level conversations.service.ts:386–399 checks caller role; non-internal roles get andWhere('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) joins org_experts.orgId so 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.app serving latest main branch railway.json configures DOCKERFILE builder + /health healthcheck. Confirmation that the URL serves latest main is operational, not visible in repo.
  • Vercel frontend deployed β€” env vars correct frontend/vercel.json has no hardcoded API URLs. api.ts:6 uses NEXT_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 undefined configs causing silent failures (see env var audit below) .env.example is comprehensive but actual Railway production values are not visible in repo. Per the audit table at the bottom, DEMO_MODE is currently true and must be flipped; STRIPE_SECRET_KEY/SLACK_SIGNING_SECRET need verification.
  • Database migrations run β€” no pending migrations on production DB 38 migrations under api/migrations/; data-source.ts:67 sets synchronize: false and migrationsRun: false (explicit, manual). app.module.ts:133–135 keeps synchronize off in production. Operational confirmation needed that npm run typeorm:run was 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 from REDIS_URL; returns null when unset and callers degrade gracefully.
  • RLS enforced β€” no cross-org data leakage (verified by attempting cross-org API calls) OrgRlsInterceptor registered globally at app.module.ts:180 via APP_INTERCEPTOR; sets app.current_org_id per 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–93 blocks /auth/demo-login when NODE_ENV==='production' OR DEMO_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 (grep fc59a4 returns 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: Twilio validateRequest(). Telegram: HMAC-SHA256. Email inbound has none (see Section 5).
  • JWT secret is production-strength β€” boot guard in place auth.module.ts:15–28 throws at startup if NODE_ENV==='production' and JWT_SECRET is unset, shorter than 32 chars, or starts with dev-. notifications.module.ts and notifications.gateway.ts use no fallback (cfg.get('JWT_SECRET') directly).
  • MASTER_ENCRYPTION_KEY is unique per env β€” staging and production keys differ common/crypto.ts:14–23 validates 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–93 double-gates demo-login on NODE_ENV and DEMO_MODE. Seed runner short-circuits in prod at seed.service.ts:70–71 (APP_ENV==='production' returns).
  • CORS locked down β€” ALLOWED_ORIGINS only includes production domains main.ts:42–56 enforces explicit origin allowlist + *.{HWORK_DOMAIN} regex; rejects unknown origins with explicit error. Demo Vercel origins only allowed when DEMO_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-deploy workflow #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; the guard workflow is a required status check that rejects PRs to main not originating from staging (and to staging not from dev). See docs/promotion-flow.md. Mandatory once ECS prod is live so emergency hotfixes can't bypass review.
  • terraform-apply is 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_MODE production guards + CI check β€” PR #1510 added a CI guard + docs that fail-closed prevent DEMO_MODE=true from 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)) β€” LagoProvider adapter 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_URL for 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 optional specialist_id on 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 by org_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_messages dropped (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.work verified in Resend (DKIM, SPF, return-path) β€” resolved 2026-06-01
  • onboarding@h.work is the From: header on invite + OTP + welcome emails (not hptestingsite.com or dev.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-rag Cloud Map service); all live docs reference Haystack, RAGflow callouts retained only as tombstones (see docs/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-rag service (#1527 Terraform; #1541 rag-deploy.yml). Dev remains on Railway via rag/railway.toml.
  • RAGFLOW_* env vars purged from prod β€” only HAYSTACK_* / 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 #7E1D1D on 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:

VariableStatusNotes
DATABASE_URLβœ… Set
JWT_SECRETβœ… SetVerify it's not the staging value
MASTER_ENCRYPTION_KEYβœ… SetMust differ from staging
OPENAI_API_KEY / OPENROUTER_API_KEYβœ… SetVerify balance / rate limits
RESEND_API_KEYβœ… SetVerify sending domain verified
REDIS_URLβœ… Set
TWILIO_ACCOUNT_SIDβœ… Set
TWILIO_API_KEY + TWILIO_API_SECRETβœ… SetPer-env key SK79b70e...
TWILIO_SUBACCOUNT_SIDβœ… SetACfc8480...
TWILIO_SUBACCOUNT_TOKENβœ… SetVerify not rotated
TWILIO_WEBHOOK_BASE_URLβœ… Sethttps://api-production-f95e.up.railway.app
STRIPE_SECRET_KEYπŸ—‘οΈ LegacyDirect Stripe is deprecated; post-cutover Stripe lives only inside Lago's PSP config. Removal tracked in #768.
LAGO_API_KEY❌ RequiredPending #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❌ RequiredSelf-hosted Lago endpoint (e.g. https://lago.h.work/api/v1). Pending #702.
SLACK_SIGNING_SECRET⚠️ CheckOptional 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βœ… falseSet 2026-05-08; verified post-deploy
NODE_ENVβœ… Should be production
ALLOWED_ORIGINSβœ… SetVerify only prod domains
HWORK_DOMAINβœ… Seth852.work (staging) / h.work (prod)
CLOUDFLARE_EMAIL_WEBHOOK_SECRETβœ… SetShared with Cloudflare email worker; HMAC verify active
R2_BUCKET_NAMEβœ… Sethwork-dev / hwork-staging / hwork (prod) β€” all confirmed exist 2026-05-10
R2_ACCOUNT_IDβœ… SetConfirmed across dev/staging/prod Railway envs
R2_ACCESS_KEY_IDβœ… SetConfirmed across dev/staging/prod Railway envs

Launch Day Sequence​

  1. T-48h β€” Full staging smoke test: AM setup β†’ client onboard β†’ WhatsApp message β†’ expert queue β†’ resolve
  2. T-24h β€” Production env var audit; rotate any test secrets; confirm DEMO_MODE=false
  3. T-12h β€” Run DB migrations on production; verify no pending
  4. T-4h β€” Deploy main to Railway production + Vercel; smoke test production URLs
  5. T-1h β€” Send test WhatsApp to production number; confirm end-to-end
  6. 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.