Feature: Channels
Channels are the communication surfaces through which a client team contacts their assigned Specialist persona. Each inbound message is normalized and routed into the conversation system regardless of source channel. Channel routing is one direction: inbound from the client, outbound from the Specialist (operated by Humanity Protocol's Expert + AI agent team).
Channel Overview
| Channel | Status | Auth Method | Notes |
|---|---|---|---|
| ✅ Live | Cloudflare Email Routing → Resend | See three-system overview below | |
| ✅ Live (Twilio) | HMAC-SHA1 (Twilio sig) | One inbound number per environment (WHATSAPP_PHONE_NUMBER); all orgs on the env share it. Sender identity (which org a phone belongs to) is established by self-service pairing codes (whatsapp_linked_numbers). Legacy whatsapp_sender_whitelist kept only as routing fallback | |
| Slack | ✅ Adapter done | HMAC-SHA256 | Blocked on Slack App credentials (BLK-004) |
| Telegram | ✅ Live | HMAC-SHA256 secret token | |
| Teams | ⚠️ Stub | Bot Framework | Needs credentials (BLK-032) |
| ⚠️ Stub | App secret | XML parsing not yet implemented |
Email
Overview
h.work uses three distinct email systems. Do not confuse them.
| System | Purpose | Provider | Who configures |
|---|---|---|---|
| Transactional | Invites, notifications, OTPs | Resend (RESEND_API_KEY) | HP internal |
| Specialist inbound | Clients email bob@{HWORK_DOMAIN}`` | Cloudflare Email Routing → webhook | HP infra |
| Expert personal | HP staff mailboxes (david@ex.{HWORK_DOMAIN}``) | Google Workspace | HP IT |
System 1 — Transactional Email (Resend)
Used for platform-generated emails: onboarding invites, OTP codes, welcome emails, AM completion notifications.
- Provider: Resend (
RESEND_API_KEYenv var, already set in Railway) - From address:
Human.work <onboarding@h852.work>(staging) →onboarding@h.workonce domain is verified in Resend - Code:
api/src/email/email.service.ts(EmailService) - Pending: h.work domain needs to be verified in Resend dashboard for production use
System 2 — Specialist Inbound Email (Cloudflare → Webhook → Resend)
⚠️ Current implementation (MVP). This system treats the Specialist email as a lightweight routing alias. The long-term plan is to replace this with full Google Workspace identities (see Long-Term Plan: Google Workspace Identities below).
Used when clients email their assigned Specialist (bob@h.work) to start or continue a conversation.
Architecture:
Client email → bob@h.work (Cloudflare MX record on h.work)
│
▼
Cloudflare Email Routing
(forward rule: *@h.work → /channels/email/inbound)
│
▼
POST /channels/email/inbound (our webhook)
│
▼
ConversationsService.findOrCreateByChannel()
│
▼
Expert queue for review
│
▼
Expert approves → Resend API sends reply from bob@h.work (DKIM)
Key details:
- Specialist email is a routing alias — not a real mailbox; Cloudflare routes inbound, Resend sends outbound
- Sender authorization is two-tier: org membership is the primary check (the FROM address must belong to a user with a row in
org_membershipsfor one of the Specialist's orgs); the legacyemail_whiteliststable is consulted only as a fallback for senders without a portal account (e.g. personal addresses) - Unknown senders receive a one-shot auto-reply ("This specialist inbox is for registered users only…") sent from the Specialist's own routing address so threading visually matches
- Reply threading via
In-Reply-Toheader — subsequent emails attach to existing conversation - Customer ID is deterministic:
email:<from_address> - Outbound replies sent via Resend API, DKIM-signed as the Specialist routing address
- Returns 200 immediately; processing is async (
setImmediate) - NOT SendGrid — old code used SendGrid Inbound Parse; this has been replaced with Cloudflare Email Routing + Resend
- All inbound/outbound email events logged to
email_audit_logtable - Per-subject threading via
In-Reply-To;channel_message_idused for dedup
Setup requirements (HP infra):
| Requirement | Details |
|---|---|
| Cloudflare Email Routing | Enable on {HWORK_DOMAIN} in Cloudflare dashboard |
| Cloudflare Worker | Deploy relay worker (see docs/ops/EMAIL-CHANNEL-SETUP.md) |
| Forward rule | *@{HWORK_DOMAIN}`` → Cloudflare Worker → POST /channels/email/inbound |
| MX record | Cloudflare sets automatically when Email Routing is enabled |
| Domain in Resend | Verify {HWORK_DOMAIN} in Resend dashboard for DKIM on outbound replies |
Per-org credentials (stored in integration_credentials table, optional override):
| Key | Description |
|---|---|
resend_api_key | Resend API key for outbound (falls back to RESEND_API_KEY env var) |
from_email | Specialist's routing address (e.g. bob@h.work) |
System 3 — Expert Personal Email (Google Workspace)
HP staff (david@ex.h.work) have personal mailboxes via Google Workspace on the ex.h.work subdomain.
- These are NOT routed through the platform
- Expert responses to clients always go out as the Specialist address (
bob@h.work), never fromdavid@ex.h.work - HP IT manages Google Workspace externally; no platform code involved
Long-Term Plan: Google Workspace Identities
Status: Planned — not yet implemented. The current Cloudflare routing alias approach is the MVP implementation. The long-term direction is full Google Workspace identities per Specialist.
Decision: Each Specialist persona (bob@h.work) will be a real Google Workspace user — not a routing alias. This gives each Specialist a complete digital identity: real mailbox, calendar, Drive, Meet, and SSO. Specialists become fully autonomous identities that agents can operate programmatically.
Why this matters:
- Clients can book calendar time directly with
bob@h.work - Agents can read inbox threads, compose and send email natively, create calendar events — all as the Specialist identity
- Google's deliverability and reputation vs. Cloudflare/Resend for cold outbound
Sign in with Google as bob@h.workbecomes possible (per-Specialist SSO)- Drive folders shareable per-client, per-Specialist
Architecture (planned):
GCP Service Account (single, platform-owned)
+ domain-wide delegation on h.work Workspace
│
├─ impersonate bob@h.work → Gmail API (send, read, search)
├─ impersonate bob@h.work → Calendar API (create events, freebusy, Meet links)
├─ impersonate bob@h.work → Drive API (share files, manage folders)
│
└─ Admin Directory API (no impersonation needed)
├─ POST /admin/directory/v1/users ← provision on Specialist creation
└─ DELETE /admin/directory/v1/users/{key} ← deprovision on removal
Key properties:
- Single service account JSON key in Railway env — no per-user OAuth tokens, no human intervention needed at runtime
- Domain-wide delegation grants are set once in the Google Admin Console by HP IT
- All Gmail/Calendar/Drive API calls impersonate the target Specialist identity
- Workspace user provisioning triggered from
SpecialistsService.create()via Admin Directory API - Cost: ~$6/user/month (Google Workspace Business Starter). Plan licences based on active Specialist count, not Expert count.
Migration path from current setup:
- Set up GCP project + service account + domain-wide delegation on
h.work - Grant scopes:
gmail.send,gmail.readonly,calendar,drive,admin.directory.user - Add
GoogleWorkspaceServiceto NestJS — wraps Admin SDK + Gmail/Calendar/Drive APIs - Wire
SpecialistsService.create()→ provision Workspace user - Migrate inbound routing: swap Cloudflare catch-all for per-user Gmail API polling or Pub/Sub push
- Retire Cloudflare Email Routing forward rule once migration is verified
What stays the same:
- Outbound always from Specialist address (now a real Gmail, not Resend)
- Persona integrity — clients still only see
bob@h.work, never the Expert's personal address email_audit_logtable continues to log all events
Sender Authorization
The inbound handler checks senders in this order:
- Org membership (primary): join
org_membershipstousersonLOWER(users.email) = <FROM>andorg_id IN (<specialist's orgs>). Most users — anyone with a portal login — are authorized this way without any extra setup. email_whitelistsexact email match (fallback): for users without a portal account who still need to email in.email_whitelistsdomain match (fallback): wildcard authorization for an entire corporate domain.- No match → unknown sender: auto-reply "This specialist inbox is for registered users only…" from the Specialist's address, then drop. Logged to
email_audit_logwithaction=auto-replied:unknown_sender.
Whitelist Management API
GET /onboarding/:token/whitelist— list entriesPOST /onboarding/:token/whitelist— add entry (during onboarding)DELETE /onboarding/:token/whitelist/:id— remove entry- Default: empty. Most orgs no longer need any whitelist entries — invited team members are authorized via
org_memberships. Add entries only for senders who don't have portal logins.
WhatsApp
WhatsApp is delivered via one Twilio number per environment (staging has its own number, production has its own). All clients on the env share that single inbound number; the Specialist/persona identity (Bob, Nova, etc.) is carried entirely at the conversation/Specialist layer, not by the dialled phone number. Sender→org identity is established at runtime via self-service pairing codes generated from the client portal.
Single-Number Model
Each environment has exactly one inbound WhatsApp number, configured via the WHATSAPP_PHONE_NUMBER env var. The platform looks up which org a message belongs to by the sender's phone (From) via whatsapp_linked_numbers, never by the receiving number (To). One number can therefore serve every org on the env.
Outbound replies always go from the same number that received the message (i.e. WHATSAPP_PHONE_NUMBER).
Legacy: the
whatsapp_number_pooltable and the per-Specialist pool-assignment flow were the v1 design. They are retained for back-compat but no longer required for routing —getOrgWhatsAppNumber()readsWHATSAPP_PHONE_NUMBERdirectly.
Pairing Code Flow (canonical sender onboarding)
- User signs into the portal → Settings → WhatsApp (
/client/settings/whatsapp/). - Dashboard generates a one-time
HW-XXXXcode (15-minute TTL, copy + refresh buttons, live countdown). Stored inwhatsapp_pairing_codeswithuserId+orgId. - User texts the code to the env's WhatsApp number on WhatsApp.
- Inbound webhook detects the
HW-XXXXpattern, consumes the code, and creates a row inwhatsapp_linked_numbers (userId, orgId, phoneE164). A confirmation reply "Connected to{OrgName}" is sent. - Future messages from that phone route automatically to the user's org conversation.
- The same Settings page lists linked numbers and lets the user unlink them.
Constraints:
- One phone ↔ one org max. Re-pairing the same phone to a different org is rejected with a "you're already connected, unlink at /client/settings/profile" reply, and the new code is not burned.
- A user may link multiple phones to the same org (e.g. personal + work).
- Invalid / expired / already-used codes get a "not recognised, generate a new one from the dashboard" reply; the code in the message is ignored, not consumed.
Inbound Routing Priority
The inbound handler (routeTwilioWhatsApp in api/src/channels/channels.controller.ts) tries these in order:
- Pairing-code message — if the body matches
^HW-[A-Z0-9]{4}$(case-insensitive) → consume code + reply confirmation (or rejection per the rules above). Checked first so a user can re-pair even if already linked. whatsapp_linked_numberslookup onFrom→ route to the linked org's conversation.whatsapp_sender_whitelistlookup (To+From) → legacy fallback for orgs that pre-date the pairing flow.- Unknown sender → auto-reply pointing to the dashboard at
https://app.{HWORK_DOMAIN}/client/settings/profile(rate-limited once per 24h per sender via Redis).
Architecture
Inbound WhatsApp message
│
▼
WhatsApp Servers (Meta)
│
▼
Twilio WhatsApp Gateway
│ POST /channels/whatsapp/inbound
│ X-Twilio-Signature (HMAC-SHA1)
▼
humanwork-api (NestJS)
│ 1. Verify Twilio HMAC-SHA1 signature
│ 2. Deduplicate via Redis (MessageSid, 1-hour TTL)
│ 3. Match against routing priority (pairing code → linked → whitelist → unknown)
│ 4. For pairing codes: write whatsapp_linked_numbers, mark code used
│ 5. For known senders: dispatch to ConversationsService / ExpertQueue
▼
ConversationsService → ExpertQueue
Tables
| Table | Purpose |
|---|---|
whatsapp_linked_numbers | Pairing result: (userId, orgId, phoneE164) — the canonical routing key |
whatsapp_pairing_codes | Live HW-XXXX codes with TTL, userId, orgId |
whatsapp_number_pool | Legacy. No longer required for routing; WHATSAPP_PHONE_NUMBER env var supersedes it. Admin endpoints remain for back-compat |
whatsapp_sender_whitelist | Deprecated. Legacy routing fallback only. Do not configure for new orgs |
Other Key Details
- Outbound replies are sent via Twilio REST API from the same number that received the message (always
WHATSAPP_PHONE_NUMBERfor the env). - Webhook:
POST /channels/whatsapp/inbound— Twilio deliversapplication/x-www-form-urlencodedpayload. - Signature verification: HMAC-SHA1.
TWILIO_AUTH_TOKEN(main account) is preferred, withTWILIO_SUBACCOUNT_TOKENas fallback for subaccount-owned senders. The env's WhatsApp number lives on the main account, so verification typically succeeds againstTWILIO_AUTH_TOKEN.
Configuration (Railway env vars)
| Variable | Description |
|---|---|
WHATSAPP_PHONE_NUMBER | E.164 inbound number for this env (one per env). All orgs share it |
TWILIO_ACCOUNT_SID | Main account SID — used for outbound REST calls (WhatsApp senders are registered on the main account) |
TWILIO_API_KEY | Main account API Key SID — used to authenticate outbound sends |
TWILIO_API_SECRET | Main account API Key Secret — paired with TWILIO_API_KEY |
TWILIO_AUTH_TOKEN | Main account Auth Token — preferred token for inbound signature verification |
TWILIO_SUBACCOUNT_SID | Per-env subaccount SID |
TWILIO_SUBACCOUNT_TOKEN | Per-env subaccount Auth Token — fallback signature verification when sender is owned by the subaccount |
TWILIO_WEBHOOK_BASE_URL | Public base URL for this env (e.g. https://api.h.work) |
For full setup, see docs/ops/TWILIO-SETUP-CHECKLIST.md.
Slack
Architecture
Slack event (app_mention or message)
│
▼
POST /channels/slack/events
│ HMAC-SHA256 signature verification
│ 5-minute replay window
▼
ConversationsService.findOrCreateByChannel()
│ customerId = "slack:{teamId}:{channelId}"
▼
Agent processes → response posted to Slack channel
- Returns 200 immediately (async processing)
- Handles
url_verificationchallenge for Slack App setup - Replay attack protection via
X-Slack-Request-Timestamp
Configuration Keys
| Key | Description |
|---|---|
bot_token | Slack Bot OAuth token (xoxb-...) |
signing_secret | From Slack App settings |
app_id | Optional — for multi-app routing |
Status: BLOCKED — requires Slack App credentials (BLK-004). Logic is fully implemented.
Telegram
Architecture
POST /channels/telegram/webhook— receives Bot API updates- Validates
X-Telegram-Bot-Api-Secret-Tokenheader (HMAC-SHA256) - Routes to conversation system
Configuration Keys
| Key | Description |
|---|---|
bot_token | Telegram Bot token |
webhook_secret | Optional HMAC secret for webhook validation |
Status: Live. Implemented in fix/issue7-telegram branch, merged to master.
Teams (Microsoft)
Stub implementation only. Bot Framework connector is wired but not tested.
POST /channels/teams/webhook— handler exists, marked// needs-testing
Status: NOT_STARTED (BLK-032). Requires Microsoft App registration + Bot Framework credentials.
WeChat
Stub implementation only.
GET /channels/wechat/webhook— verification challenge handlerPOST /channels/wechat/webhook— inbound stub (XML parsing not implemented)
Status: NOT_STARTED (BLK-042, P3). Requires WeChat Official Account registration.
Channel Admin Endpoints
These endpoints provide operational visibility for org admins:
| Endpoint | Description |
|---|---|
GET /orgs/:orgId/channels/status | Health status for all configured channels |
GET /orgs/:orgId/channels/webhook-urls | Exact webhook URLs to paste into channel dashboards |
Integration Credentials Management
All per-org channel credentials are stored encrypted in integration_credentials (AES-256-GCM). API responses mask credential values (first 4 chars + ****).
| Endpoint | Description |
|---|---|
POST /orgs/:orgId/integrations | Create or upsert channel credentials |
GET /orgs/:orgId/integrations | List integrations (masked) |
DELETE /orgs/:orgId/integrations/:id | Remove integration |
POST /orgs/:orgId/integrations/:id/test | Test connection (lightweight API call) |
Roadmap
| Channel | Timeline | Notes |
|---|---|---|
| P3 | Requires WeChat Official Account | |
| Teams | P2 | BLK-032 — needs Microsoft App registration + Bot Framework |