Deployment¶
A practical checklist for taking a jmaple instance from development to production.
Process model¶
jmaple.app:app is a stock FastAPI application. Run it under any ASGI server:
Workers are independent — each holds its own in-process PushBroker and
SQLAlchemy connection pool. State that needs to be shared lives in the
database; everything else is per-worker.
Database¶
PostgreSQL via asyncpg is the recommended driver:
Run migrations as part of your deploy pipeline:
Never start the server without applying pending migrations first.
Blob storage¶
Default is the filesystem at JMAPLE_BLOB_DIR. For multi-instance
deployments, swap jmaple.blobs.store.BlobStore with an S3-backed
implementation. The protocol is three methods:
class BlobStore(Protocol):
async def put(self, account_id: str, blob_id: str, stream: AsyncIterator[bytes]) -> tuple[str, int, str]: ...
async def open(self, storage_key: str) -> AsyncIterator[bytes]: ...
async def delete(self, storage_key: str) -> None: ...
Override jmaple.blobs.deps.get_blob_store() to return your implementation.
Reverse proxy¶
Specifics for Nginx-like proxies:
/jmap/eventsource— long-livedtext/event-stream. Disable response buffering:proxy_buffering off;./jmap/ws— WebSocket upgrade. PassUpgrade/Connectionheaders, bumpproxy_read_timeout./jmap/upload/*— POSTs of arbitrary size up toJMAPLE_CORE_CAPABILITY__MAX_SIZE_REQUEST. Make sureclient_max_body_sizematches.
Observability¶
- Python's
loggingmodule — wire up to your usual handlers. - Set
JMAPLE_DEBUG=truefor verbose request/response logs in dev only. - Every method call writes a structured line with the method name and call id.
Push¶
The default in-process PushBroker is single-instance. For multi-worker /
multi-instance setups, you have three options:
- Sticky sessions on the load balancer — SSE/WS clients always hit the same worker. Simplest; doesn't help webhooks.
- Webhook-only — backend services subscribe via
PushSubscription/set, skip SSE/WS entirely. Works across any number of workers. - Replace the broker — implement a Redis-backed
PushBrokerand patchjmaple.push.broker.get_broker()to return it. Webhooks and SSE/WS keep working unchanged.
Health checks¶
There's no built-in /healthz — the JMAP session resource is the de-facto
liveness probe. A 401 means the server is up but unauthenticated; a 200
means everything's healthy.
For a stricter check, hit /.well-known/jmap with a known-good bearer token
from your monitoring pipeline.