Skip to main content

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

ChannelStatusAuth MethodNotes
Email✅ LiveCloudflare Email Routing → ResendSee three-system overview below
WhatsApp✅ 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 doneHMAC-SHA256Blocked on Slack App credentials (BLK-004)
Telegram✅ LiveHMAC-SHA256 secret token
Teams⚠️ StubBot FrameworkNeeds credentials (BLK-032)
WeChat⚠️ StubApp secretXML parsing not yet implemented

Email

Overview

h.work uses three distinct email systems. Do not confuse them.

SystemPurposeProviderWho configures
TransactionalInvites, notifications, OTPsResend (RESEND_API_KEY)HP internal
Specialist inboundClients email bob@{HWORK_DOMAIN}``Cloudflare Email Routing → webhookHP infra
Expert personalHP staff mailboxes (david@ex.{HWORK_DOMAIN}``)Google WorkspaceHP IT

System 1 — Transactional Email (Resend)

Used for platform-generated emails: onboarding invites, OTP codes, welcome emails, AM completion notifications.

  • Provider: Resend (RESEND_API_KEY env var, already set in Railway)
  • From address: Human.work <onboarding@h852.work> (staging) → onboarding@h.work once 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_memberships for one of the Specialist's orgs); the legacy email_whitelists table 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-To header — 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_log table
  • Per-subject threading via In-Reply-To; channel_message_id used for dedup

Setup requirements (HP infra):

RequirementDetails
Cloudflare Email RoutingEnable on {HWORK_DOMAIN} in Cloudflare dashboard
Cloudflare WorkerDeploy relay worker (see docs/ops/EMAIL-CHANNEL-SETUP.md)
Forward rule*@{HWORK_DOMAIN}`` → Cloudflare Worker → POST /channels/email/inbound
MX recordCloudflare sets automatically when Email Routing is enabled
Domain in ResendVerify {HWORK_DOMAIN} in Resend dashboard for DKIM on outbound replies

Per-org credentials (stored in integration_credentials table, optional override):

KeyDescription
resend_api_keyResend API key for outbound (falls back to RESEND_API_KEY env var)
from_emailSpecialist'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 from david@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.work becomes 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:

  1. Set up GCP project + service account + domain-wide delegation on h.work
  2. Grant scopes: gmail.send, gmail.readonly, calendar, drive, admin.directory.user
  3. Add GoogleWorkspaceService to NestJS — wraps Admin SDK + Gmail/Calendar/Drive APIs
  4. Wire SpecialistsService.create() → provision Workspace user
  5. Migrate inbound routing: swap Cloudflare catch-all for per-user Gmail API polling or Pub/Sub push
  6. 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_log table continues to log all events

Sender Authorization

The inbound handler checks senders in this order:

  1. Org membership (primary): join org_memberships to users on LOWER(users.email) = <FROM> and org_id IN (<specialist's orgs>). Most users — anyone with a portal login — are authorized this way without any extra setup.
  2. email_whitelists exact email match (fallback): for users without a portal account who still need to email in.
  3. email_whitelists domain match (fallback): wildcard authorization for an entire corporate domain.
  4. 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_log with action=auto-replied:unknown_sender.

Whitelist Management API

  • GET /onboarding/:token/whitelist — list entries
  • POST /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_pool table and the per-Specialist pool-assignment flow were the v1 design. They are retained for back-compat but no longer required for routing — getOrgWhatsAppNumber() reads WHATSAPP_PHONE_NUMBER directly.

Pairing Code Flow (canonical sender onboarding)

  1. User signs into the portal → Settings → WhatsApp (/client/settings/whatsapp/).
  2. Dashboard generates a one-time HW-XXXX code (15-minute TTL, copy + refresh buttons, live countdown). Stored in whatsapp_pairing_codes with userId + orgId.
  3. User texts the code to the env's WhatsApp number on WhatsApp.
  4. Inbound webhook detects the HW-XXXX pattern, consumes the code, and creates a row in whatsapp_linked_numbers (userId, orgId, phoneE164). A confirmation reply "Connected to {OrgName}" is sent.
  5. Future messages from that phone route automatically to the user's org conversation.
  6. 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:

  1. 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.
  2. whatsapp_linked_numbers lookup on From → route to the linked org's conversation.
  3. whatsapp_sender_whitelist lookup (To + From) → legacy fallback for orgs that pre-date the pairing flow.
  4. 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

TablePurpose
whatsapp_linked_numbersPairing result: (userId, orgId, phoneE164) — the canonical routing key
whatsapp_pairing_codesLive HW-XXXX codes with TTL, userId, orgId
whatsapp_number_poolLegacy. No longer required for routing; WHATSAPP_PHONE_NUMBER env var supersedes it. Admin endpoints remain for back-compat
whatsapp_sender_whitelistDeprecated. 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_NUMBER for the env).
  • Webhook: POST /channels/whatsapp/inbound — Twilio delivers application/x-www-form-urlencoded payload.
  • Signature verification: HMAC-SHA1. TWILIO_AUTH_TOKEN (main account) is preferred, with TWILIO_SUBACCOUNT_TOKEN as fallback for subaccount-owned senders. The env's WhatsApp number lives on the main account, so verification typically succeeds against TWILIO_AUTH_TOKEN.

Configuration (Railway env vars)

VariableDescription
WHATSAPP_PHONE_NUMBERE.164 inbound number for this env (one per env). All orgs share it
TWILIO_ACCOUNT_SIDMain account SID — used for outbound REST calls (WhatsApp senders are registered on the main account)
TWILIO_API_KEYMain account API Key SID — used to authenticate outbound sends
TWILIO_API_SECRETMain account API Key Secret — paired with TWILIO_API_KEY
TWILIO_AUTH_TOKENMain account Auth Token — preferred token for inbound signature verification
TWILIO_SUBACCOUNT_SIDPer-env subaccount SID
TWILIO_SUBACCOUNT_TOKENPer-env subaccount Auth Token — fallback signature verification when sender is owned by the subaccount
TWILIO_WEBHOOK_BASE_URLPublic 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_verification challenge for Slack App setup
  • Replay attack protection via X-Slack-Request-Timestamp

Configuration Keys

KeyDescription
bot_tokenSlack Bot OAuth token (xoxb-...)
signing_secretFrom Slack App settings
app_idOptional — 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-Token header (HMAC-SHA256)
  • Routes to conversation system

Configuration Keys

KeyDescription
bot_tokenTelegram Bot token
webhook_secretOptional 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 handler
  • POST /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:

EndpointDescription
GET /orgs/:orgId/channels/statusHealth status for all configured channels
GET /orgs/:orgId/channels/webhook-urlsExact 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 + ****).

EndpointDescription
POST /orgs/:orgId/integrationsCreate or upsert channel credentials
GET /orgs/:orgId/integrationsList integrations (masked)
DELETE /orgs/:orgId/integrations/:idRemove integration
POST /orgs/:orgId/integrations/:id/testTest connection (lightweight API call)

Roadmap

ChannelTimelineNotes
WeChatP3Requires WeChat Official Account
TeamsP2BLK-032 — needs Microsoft App registration + Bot Framework