wats.sh
Concepts

Architecture Overview

How the WATS packages, Graph request flow, and webhook flow fit together.

active · applies to 0.3.x-alpha-tooling · reviewed 2026-05-01

WATS is a Bun-first TypeScript toolkit for WhatsApp operations. It is built as small packages with explicit dependency direction, not one large framework package. You take the layers you need and ignore the rest.

Package layers

@wats/types
  shared domain contracts

@wats/crypto        @wats/graph
  crypto seam         graph client
  adapters            endpoints

@wats/core
  typed updates, filters, router, listeners, WhatsApp facade

@wats/core is the composition root: it composes @wats/graph, @wats/crypto, and @wats/types and never imports the application-edge packages. Application-edge packages sit ABOVE @wats/core and compose it — they normalize raw inputs into typed values and dispatch them:

@wats/http    webhook HTTP boundary — verifies signatures and normalizes
              raw webhooks into typed updates before dispatch (composes
              @wats/core + @wats/crypto)
@wats/config  config schema, YAML/JSON loading, env-secret references
@wats/service standalone webhook/API service, OpenAPI 3.1
@wats/cli     init, validate, doctor, serve, openapi commands

App-layer packages may depend on @wats/core; lower-level packages must not. The dependency arrows only point down — @wats/core never imports @wats/http. (@wats/http is an application-edge package, NOT a peer of @wats/graph: it sits above @wats/core and uses normalizeWebhookEnvelope from it.)

Request flow

A Graph request starts in either user code or a scoped client:

  1. PhoneNumberClient, WABAClient, or a custom defineEndpoint callable builds path/query/body options.
  2. GraphClient validates the path, query, headers, base URL, API version, token, and body handling.
  3. The injected Transport sends the request.
  4. Graph failures map into the typed error taxonomy. Catch by instanceof, not by parsing Meta's prose.

Endpoint breadth grows without duplicating transport, auth, validation, or error plumbing.

Webhook flow

A webhook request enters through @wats/http:

  1. A runtime wrapper adapts Bun, Node, or Fetch Request shapes to the runtime-neutral WebhookAdapter.
  2. The adapter verifies the Meta challenge or X-Hub-Signature-256 signature.
  3. normalizeWebhookEnvelope turns the raw JSON body into TypedUpdate values.
  4. The supplied facade-shaped object dispatches each update.
  5. TypedRouter evaluates listeners and handlers, preserving dispatch reports and isolating user-code failures.

The adapter acknowledges valid webhooks even when your handlers throw. Meta retries unacknowledged webhooks until you acknowledge them or die; a bug in your handler should not trigger that storm.

Extension points

  • Transport for retries, tracing, auth refresh, mocks, and offline tests.
  • defineEndpoint for adding typed Graph endpoints ahead of first-class wrappers.
  • TypedFilter and filter combinators for handler/listener matching.
  • RouterObserver and webhook logger hooks for observability.
  • CryptoProvider for Node/Bun/WebCrypto portability.

Invariants

  • Public API names are camelCase-only.
  • Public APIs are async-only where work may cross I/O or runtime seams.
  • Low-level packages stay runtime-portable; runtime-specific code is isolated behind adapters.
  • Consumer fixtures import through package specifiers, never relative source paths.

See also:

On this page