Nirav PatelandClaude Opus 5 6b3a09ec1c
CI / test (push) Successful in 5s
Say when a channel cannot be checked, instead of skipping it
The estate's SendGrid key is send-only: mail.send and the batch scopes,
with no suppression.read, no bounces.read, no stats. So email delivery
cannot be reconciled the way SMS now is.

The tempting move is to check SMS and quietly say nothing about email.
That is precisely how a gap disappears - the daily report looks clean
and the unchecked channel stops being a question anyone asks. It now
reports 'email delivery NOT VERIFIED' with the exact scope that would
fix it, every day, until somebody does.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-18 20:50:37 -04:00
2026-08-18 15:41:28 -04:00
2026-08-18 15:41:28 -04:00
2026-08-18 15:41:28 -04:00
2026-08-18 15:41:28 -04:00

channel-exit

The estate's one way out to a person's inbox or phone.

Services that need to reach a human POST here instead of carrying a SendGrid key of their own. Credentials live in one place, one service can be turned off or rate-limited, and a bug in some worker cannot become a bill.

The contract

POST /send
Authorization: Bearer <CHANNEL_EXIT_TOKEN>
Content-Type: application/json

{"channel": "email|sms|whatsapp|push", "to": "...", "subject": "...", "body": "..."}
Reply When
200 the provider accepted it — {"ok": true, "provider_status": 202, "id": "..."}
400 unknown channel, or to is not an email / not E.164, or nothing to send
401 missing or wrong bearer token
501 whatsapp and push — no provider for them in this estate
502 the provider refused or was unreachable; its status is in the body
503 that channel's credentials are not configured here

GET /healthz needs no token and reports which channels are wired.

The shape is fixed by its first consumer, reminders-service (app/alerts.py: gateway_sender), which treats any 2xx as delivered.

Two rules

It publishes one host port, on 127.0.0.1, and nothing else. An authenticated relay reachable from the internet is an open spam relay the moment the token leaks — and that token is copied into every consumer's environment. It gets no 0.0.0.0 binding, no bridge-IP binding, and no cloudflared route, ever.

It does not log what it was asked to send. The log records channel, a redacted address, the provider's status and message id. Never a subject, never a body, never a credential.

Consumers

From the host: http://127.0.0.1:<CHANNEL_EXIT_PORT>/send

From another container: http://channel-exit:8080/send. The relay joins the docker network its consumers already run on (CHANNEL_EXIT_NETWORK) and is reached there by name, on the container port — CHANNEL_EXIT_PORT is a host-side publish and does not exist inside that network.

A consumer's own 127.0.0.1 is that consumer, so a loopback gateway URL fails every delivery while looking correctly configured. That is the one mistake to watch for here.

Running it

cp .env.example .env && chmod 600 .env    # fill in, then
docker compose up -d --build

Every value in .env is required except PROVIDER_TIMEOUT. CHANNEL_EXIT_TOKEN has no default: the service refuses to start without one rather than coming up as an unauthenticated relay.

Tests

pip install -r requirements-dev.txt && python -m pytest -q

The suite replaces urllib.request.urlopen with something that raises, so no test can send a real message however it is written.

S
Description
The estate one way out to a person inbox or phone. Loopback-only relay for SendGrid and Twilio.
Readme
61 KiB
Languages
Python 97.6%
Dockerfile 2.4%