Echo is a tenant-aware, WhatsApp-first support platform. The monorepo contains:
apps/web: TanStack Start operator interface (Cloudflare Workers)apps/api: Fastify API and future Better Auth hostapps/worker: background processing runtimepackages/domain: shared domain contracts and ULID generationpackages/db: Drizzle schema, migrations, and Postgres clientpackages/config: shared server environment validation
This section tracks what is actually built and tested, not the product vision. See docs/architecture.md for the system diagram.
- Authentication & organizations. Better Auth-backed sign-up, sign-in, invitations, and member management, covered by
apps/api/src/modules/auth/auth.integration.test.ts. - WhatsApp channel provisioning. A CLI command (
channel:provision:twilio) creates and encrypts per-organization Twilio subaccount credentials, verifies the WhatsApp Sender isONLINE, and writes an immutable audit event. Covered bychannel-connections.integration.test.ts. - Inbound WhatsApp ingestion. Twilio webhook signature verification, message normalization, and conversation threading, covered by
twilio-webhook.test.tsandinbound-message.integration.test.ts. Inbound processing and outbound delivery are dispatched as BullMQ jobs (process-inbound-message,send-outbound-message) consumed byapps/worker. - Support inbox. Paginated conversation listing with cursor-based pagination, conversation detail, and operator replies, covered by
support-inbox.integration.test.tsandcreate-operator-reply.test.ts. The web UI exposes this at/inboxand/inbox/$conversationId. - Knowledge base. Organization owners and admins can ingest text or supported files, edit document titles, and replace or delete documents at
/settings/knowledge. The knowledge page has a separate Preview tab for testing a question through the Support Agent graph without sending a message or changing a conversation; it shows the graph's decision and any retrieved sources. - Operational health. Both the API and worker expose
/health/liveand/health/readyprobes that fail closed when Postgres or Redis is unavailable, and only report sanitized dependency state. - CI.
.github/workflows/ci.ymlruns lint, typecheck, test, and build on every pull request and push tomain.
The worker contains a Support Agent graph for inbound first-line responses. Its presence in the codebase does not establish that this version is deployed in production. Meta Embedded Signup is not built; channel provisioning remains an internal CLI operation. Echo has not been verified against real customer traffic.
- Node.js 20 or newer
- pnpm 10.33.4
- Docker with Compose
pnpm install
cp apps/api/.env.example apps/api/.env
cp apps/worker/.env.example apps/worker/.env
pnpm infra:up
pnpm db:migrate
pnpm devTo use the knowledge-base preview, set AI_GATEWAY_API_KEY and AI_CHAT_MODEL in apps/api/.env. The preview endpoint returns HTTP 503 when AI Gateway access is unavailable.
The local services use these addresses:
| Service | Address |
|---|---|
| Web | http://localhost:3000 |
| API | http://localhost:3001 |
| Worker health | http://localhost:3002 |
| Postgres | localhost:5432 |
| Redis | localhost:6379 |
To trace inbound support-agent runs, set LANGSMITH_TRACING=true and LANGSMITH_API_KEY in apps/worker/.env. Tracing is off by default; LANGSMITH_PROJECT=echo in the example selects the project. Traces include the customer message, retrieved snippets, model call, and filterable organization, conversation, and message IDs.
To verify locally, start the worker with pnpm --filter @workspace/worker dev, send a text message through a configured WhatsApp connection, and inspect the support-agent run in LangSmith.
pnpm --filter @workspace/agent eval uploads golden set v1 and runs a LangSmith experiment against the current decide step. It needs AI_GATEWAY_API_KEY and LANGSMITH_API_KEY in apps/worker/.env. See packages/agent/src/evals/README.md for the rubric and how to add a case.
Both server runtimes fail during startup if Postgres or Redis is unavailable. Once pnpm dev is running, verify their probes:
curl --fail http://localhost:3001/health/live
curl --fail http://localhost:3001/health/ready
curl --fail http://localhost:3002/health/live
curl --fail http://localhost:3002/health/readyReadiness returns HTTP 503 when either dependency is unavailable. Responses expose only sanitized dependency states.
The Better Auth schema is generated from the API-owned auth configuration. Domain tables are maintained separately in packages/db/src/schema/support.ts.
pnpm auth:schema:generate
pnpm db:generate
pnpm db:migrate
pnpm db:studioCommit generated SQL migrations. Do not use drizzle-kit push as the normal schema workflow.
pnpm db:migrate lists the migrations it applies, or reports that nothing is pending.
Database commands load apps/api/.env, the app-local environment owned by the API. The same commands can be run from packages/db as pnpm db:generate, pnpm db:migrate, and pnpm db:studio.
The initial onboarding flow uses one Twilio subaccount per Organization. Provisioning is an internal operation until Meta Embedded Signup replaces it.
Apply migrations first, then run the provisioning command. It prompts for the subaccount Auth Token using masked terminal input, so the token is not stored in shell history:
pnpm --filter @workspace/api channel:provision:twilio -- \
--organization-id 01K1EDN69NFBWCG42B2H99V2C1 \
--name "WhatsApp Support" \
--address +5511999999999 \
--account-sid AC00000000000000000000000000000000When stdin is not an interactive terminal, the command still accepts the Auth Token from stdin for controlled automation. Never pass it as a command-line argument.
The command verifies that the subaccount exposes an ONLINE WhatsApp Sender for the address, encrypts the Auth Token, activates the provider-neutral Channel Connection, and writes an immutable Audit Event. Rerunning it updates the existing connection rather than creating a duplicate.
PUBLIC_API_URL must be the externally visible API origin used in Twilio's webhook configuration. Twilio signs the exact callback URL, so a proxy-only internal origin cannot be used for signature validation.
Generate a deployment-specific credential encryption key with openssl rand -base64 32. Keep the key outside the database and increment CHANNEL_CREDENTIALS_KEY_VERSION when deliberately rotating it.
pnpm lint
pnpm typecheck
pnpm test
pnpm test:integration
pnpm buildIntegration tests start isolated Postgres and Redis containers and do not modify the local development database.
pnpm infra:down