Skip to content

Architecture

sarmakska edited this page May 3, 2026 · 2 revisions

Architecture

Stateless. No database. Logs to stdout. Trivially deployable.

Diagram

graph TD
  EXT[External Service<br/>Stripe / GitHub / Cal.com] -->|POST JSON| API[Express /hooks/:source]
  API -->|optional HMAC check| SIG{Signature ok?}
  SIG -->|no| REJ[401]
  SIG -->|yes| TPL{Template exists?}
  TPL -->|src/templates/source.js| FMT[Custom formatter]
  TPL -->|no template| DEF[Default formatter]
  FMT --> EM
  DEF --> EM[Resend send email]
  EM --> SL{Slack enabled?}
  SL -->|yes| SLACK[POST to Slack webhook]
  SL -->|no| END[Return 200]
  SLACK --> END

  classDef ext fill:#a78bfa,stroke:#a78bfa,color:#fff
  class EXT,EM,SLACK ext
Loading

Request flow

sequenceDiagram
  participant Ext as External Service
  participant API as /hooks/source
  participant V as HMAC verify
  participant T as Template
  participant R as Resend
  participant S as Slack

  Ext->>API: POST /hooks/stripe with body + X-Signature
  API->>V: verify(body, secret, signature)
  V-->>API: ok
  API->>T: format(payload)
  T-->>API: { subject, text, html }
  API->>R: send email
  R-->>API: 200
  alt Slack enabled
    API->>S: POST formatted message
  end
  API-->>Ext: 200 { ok: true }
Loading

Components

File Responsibility
src/index.js Express app, routing, signature verification, retry orchestration
src/templates/<source>.js Per-source formatters (Stripe, GitHub, Cal.com, etc.)
Dockerfile Alpine Node 20 image, ~80MB
docker-compose.yml One-command deploy with health check

Why each piece

Stateless by design. No database means no migrations, no connection pool, no failures from database outages. If you need a queue, put one in front of this service.

Single retry on 5xx. Resend has occasional transient 5xx errors. One retry with 500ms backoff catches almost all of them. More retries belong in a queue.

Per-source templates. Webhook payloads vary wildly. Stripe's invoice.paid is nothing like GitHub's push. A template per source keeps formatting logic isolated and easy to add new sources.

Default formatter (pretty JSON). When no template exists, just send the JSON as a code block. Means you can plug a brand new webhook in with zero config and start receiving emails.

HMAC verify is optional. For internal sources (your own cron jobs), you don't need it. For Stripe / GitHub, you must. The header parsing handles sha256=<hex>, <hex>, X-Signature, X-Hub-Signature-256, X-Stripe-Signature.

Resource usage

Single-process Node. ~50MB RSS at idle. ~80MB under load (10 req/sec sustained).

Handles ~1000 req/sec on a single CPU core before Resend rate-limits become the bottleneck. The bottleneck is always Resend, not this service.

Clone this wiki locally