11 KiB
MessageWeave deployment guide
Use this guide to deploy a backend, configure mail delivery, and verify notifications. Choose any platform that can run the backend container, provide encrypted environment secrets, and connect to an externally managed Redis service. The optional Cloudflare Worker gateway adds a stable URL in front of one or more backends. Platform-specific settings are in the deployment platform reference.
0. Before deployment
Prepare an externally managed Redis service with persistence enabled and a public HTTPS URL
that reaches the backend. The platform or proxy terminates TLS; MessageWeave listens on one
HTTP port (PORT, default 8080). The process needs two encrypted boot secrets:
REDIS_URL and CONFIG_ENCRYPTION_KEY (a random 32-byte key encoded as 64 hexadecimal
characters). Never put real values in source control, an image, or logs. Business settings
are saved in Redis through the protected configuration API; adding them as environment
variables does not configure the service.
Production has no local database, data volume, or internal scheduler. Full security boundaries
and prohibited modes are in charter.md.
1. Deploy a backend
Container platform
Build the root Dockerfile and configure the required secrets in the platform. For a local
container smoke run, copy .env.example to an untracked .env, replace both placeholders,
then run:
docker build -t messageweave:latest .
docker run --rm --env-file .env -p 8080:8080 messageweave:latest
The image uses a Rust builder and a Debian slim runtime, runs as a non-root user, and exposes
only port 8080. Do not mount a data or log volume. Full image contents are in the
platform reference.
2. First-time configuration
Open the backend's configuration page at its origin for initial setup. Use
CONFIG_ENCRYPTION_KEY to establish the admin session, then complete the one-time bootstrap
form. The bootstrap route is not exposed by the Worker gateway. The one-time bootstrap and
admin-session handlers are src/notify.rs:834 and src/notify.rs:1187; saving business
configuration uses src/notify.rs:694. Later edits can use the protected page through the Worker URL.
Configure at least the Telegram bot token, target chat ID, inbound chat allowlist, Stalwart JMAP HTTPS session URL and application password, webhook secret, and worker token. Set a reconcile token if using scheduled reconciliation. Configure LLM settings only when users have explicitly consented to external processing. Secret values are not returned by the configuration API; the full field semantics are in the configuration reference.
2.1 Optional remote diagnostics
Leave DEBUG_ENABLED and DEBUG_TOKEN unset in production unless remote diagnostics are
deliberately needed. Both must be configured to mount the diagnostic routes, and those routes
are reachable only through the backend origin. The diagnostic surface and response details
are recorded in the reference.
3. Build and run the Docker image
The root Dockerfile is the container deployment path. It has separate Rust builder and Debian slim runtime stages; see §8.2 for the runtime contents.
4.1 Register Telegram and Stalwart callbacks
After saving business configuration, use the Register Telegram and Stalwart callbacks
section in the configuration page. Enter the public HTTPS origin that receives callbacks:
the Worker URL when the gateway is enabled, otherwise the backend origin. The page registers
/webhook/tg with Telegram and /push/jmap with Stalwart. Credentials remain in the backend;
the browser sends only the callback URLs. The protected registration handlers are
src/notify.rs:1623 for Telegram and src/notify.rs:1468 for Stalwart.
Telegram's secret token and allowed_updates: ["message"] are taken from the saved business
configuration. Stalwart verification and verification-code writeback happen automatically
after registration. Registration can be repeated safely; registering the same origin is
idempotent. Changing the callback origin updates Telegram (atomic replacement) and, for
Stalwart, destroys the previous subscription before creating the new one under the registration
lock, so the old origin stops receiving pushes.
To remove a subscription, call POST /api/push/disable with the same callback_url.
5. Runtime configuration
Keep boot-time process configuration separate from Redis-resident business configuration.
Only REDIS_URL and CONFIG_ENCRYPTION_KEY are required at process startup. The full
business field list is in the configuration reference.
6. Short-request runtime and scheduling
The service handles one request at a time and does not keep JMAP event streams, Telegram long polling, or a background timer open. Telegram/JMAP push provide the fast path; the external scheduler invokes the endpoints described below.
6.3 Schedule queue draining and reconciliation
The backend has no internal scheduler. An external cron, scheduled job, or platform timer
must call scripts/cron-drain.sh; otherwise queued notifications
remain undelivered. The script calls /reconcile (src/notify.rs:268) when
MW_RECONCILE_TOKEN is set, then drains the worker queue through /worker
(src/notify.rs:390).
Provide MW_APP_URL and MW_WORKER_TOKEN; set MW_RECONCILE_TOKEN when reconciliation is
enabled. Store tokens in the scheduler's secret manager. For a one-shot run:
With the tokens injected by the scheduler's secret manager, run:
MW_APP_URL='https://<PUBLIC_URL>' bash scripts/cron-drain.sh --once
The reconciliation cadence is normally 5–10 minutes. Both successful endpoints return 204
with an empty body; do not treat that as an error. The script's --diagnose mode helps
separate service configuration problems from scheduling problems. Its arguments and
operating modes are listed in the scheduler reference.
6.4 Reliability behavior
The Worker retries only timeouts and backend 5xx responses; it returns 4xx responses
directly. /reconcile and /worker have longer per-route timeouts and do not fail over to a
second backend, avoiding duplicate work. Backend queue processing is at least once and relies
on Redis-backed idempotency. See the route and state reference for the complete behavior.
6.5 Delivery objective
The service permits short notification delays and targets at least 99.9% notification
availability. Redis availability and the external scheduling interval are outside the
application's control; exact delivery guarantees are described in NFR-NOTIFY-SLA and
NFR-RECONCILE-INTERVAL in the project charter.
7. Health checks
Use /healthz for liveness (src/notify.rs:118) and /ready for dependency readiness
(src/notify.rs:193). The verification sequence is in §8.1.
8.1 Verify the deployment
Check the backend first:
| Request | Expected result | Meaning |
|---|---|---|
GET /healthz |
200 |
The process is alive. |
GET /ready |
200 |
Configuration, Redis, JMAP, and Telegram probes are ready; dependency failures return 503. |
GET /api/status |
ready: true |
Required boot settings are present; missing identifies absent names. |
When the Worker gateway is enabled, also request GET /healthz-worker. Require
available >= 1 and check that version matches the deployed LB_VERSION; HTTP 200 alone
can mean no-backends. The cloudflare-worker/ directory's tests and deploy command are
described in its README.
Send /help to the configured Telegram chat and confirm a reply. Then send a new email and
confirm that the external scheduler drains its notification. A healthy /healthz only proves
process liveness; it does not prove that Redis or either upstream is usable.
8.2 Docker image details
The root Dockerfile's builder uses rust:1-slim-bookworm; the runtime is
debian:bookworm-slim. The runtime installs CA certificates, tini, curl, procps,
iproute2, jq, and netcat-openbsd, creates the messageweave system user, sets
PORT=8080, and starts the binary through tini. It has no VOLUME declaration and does
not start Redis. These details describe the repository Docker image; other platforms may provide their own
builder and runtime image.
9. Confirmed deployment choices
These deployment decisions are settled; they are not per-installation prerequisites.
9.1 Public ingress and state
The backend uses an external HTTPS ingress and external Redis. Telegram, JMAP Push, and scheduled requests use the selected public URL. When the Worker gateway is enabled, register that stable Worker URL with Telegram and Stalwart; do not expose credentials at the gateway. The stable IDs in this section point to the project charter's registry.
10. Optional Cloudflare Worker gateway
The Worker provides one public HTTPS entry point for multiple backend origins. All backends
must share the same Redis and business configuration. The Worker forwards only its fixed
route allowlist; bootstrap and /debug/* remain origin-only. Detailed route facts are in
the gateway route matrix.
10.1 Topology
Telegram webhooks, Stalwart push callbacks, and external scheduling can use the stable Worker URL. The Worker forwards to backend HTTPS origins; it does not connect to Redis or JMAP.
10.2 Multi-instance prerequisites
Multiple backends can share traffic because request state is stored in Redis, duplicate deliveries are deduplicated, and reconciliation repairs missed push events. No sticky session or application code change is required.
10.3 Trust model
The Worker passes request headers and bodies through. Backend authentication remains required because backend origins may be directly reachable. Every instance must use the same shared business credentials and Redis state.
10.4 Routes and failover
HTTP requests to the Worker receive a permanent 308 redirect to the same HTTPS URL before
route validation or forwarding. The Worker rejects unregistered routes with 404 and wrong
methods with 405. Ordinary requests try at most LB_MAX_ATTEMPTS origins (default two), and
only a timeout or 5xx triggers failover. /reconcile and /worker are single-attempt routes with longer timeout
overrides. Use the route matrix and tuning values in the reference.
10.5 Callback URLs
When using the gateway, set Telegram's webhook and Stalwart's push callback to the Worker domain. The external scheduler can use the same URL for forwarded paths. Bootstrap and diagnostic endpoints still require the backend origin.
10.6 Gateway health
GET /healthz-worker reports the Worker version and backend availability. Use both
available and version to verify a deployment; status: no-backends can be returned with
HTTP 200 when no valid origin is configured.
10.7 Boundaries
The Worker is not a Redis proxy and does not make the backend stateless; shared external Redis remains required. It does not add a database, a long-lived connection, or a second layer of business authentication.