Skip to content

7. Deploying jmaple

You've built a capability and it works locally. Now ship it.

Build a wheel

uv build produces a wheel and an sdist in dist/:

just package
ls dist/
# jmaple-0.1.0-py3-none-any.whl  jmaple-0.1.0.tar.gz

Pick a database

SQLite is fine for development but won't survive production. Use PostgreSQL:

JMAPLE_DATABASE_URL=postgresql+asyncpg://jmaple:secret@db.internal/jmaple

The async driver asyncpg ships as a dependency.

Run with workers

uv run jmaple serve defaults to one worker for dev. In production, hand off to a process manager — gunicorn with uvicorn workers is the common choice:

gunicorn jmaple.app:app \
  -k uvicorn.workers.UvicornWorker \
  -w 4 \
  -b 0.0.0.0:8000 \
  --access-logfile -

Run migrations on deploy

uv run jmaple migrate upgrade head

Run this before starting the server, ideally as a step in your deploy pipeline. Bundle your plugin's Alembic revision in the same package so operators run one command, not two.

Configure auth

For an external IdP, use the JWT or OIDC provider:

JMAPLE_AUTH__PROVIDERS=["jwt"]
JMAPLE_AUTH__JWT__ISSUER=https://issuer.example.com/
JMAPLE_AUTH__JWT__AUDIENCE=jmaple
JMAPLE_AUTH__JWT__JWKS_URL=https://issuer.example.com/.well-known/jwks.json

Bootstrap the first admin by listing their composite id:

JMAPLE_AUTH__ADMIN_SUBJECTS=["https://issuer.example.com/|alice"]

The first time alice authenticates, the framework auto-provisions her user record and grants her the admin role on the system account.

Blob storage

The default is the filesystem (JMAPLE_BLOB_DIR=./var/blobs). For multi-instance deployments, wrap or replace jmaple.blobs.store.BlobStore with an S3-backed implementation. The protocol is small: put, open, delete.

Observability

  • The framework uses Python's logging module — configure handlers as you would any FastAPI app.
  • Every JMAP method call writes structured log lines including the method name and call id.
  • Failed handlers log full tracebacks at WARNING; request-level errors get ERROR.

What's behind a load balancer

  • /jmap and /.well-known/jmap — JSON request/response, normal HTTP.
  • /jmap/upload/* — POST with arbitrary Content-Type, no chunked encoding required.
  • /jmap/download/* — GET, streams the blob.
  • /jmap/eventsource — long-lived text/event-stream, set proxy_buffering off in Nginx.
  • /jmap/ws — WebSocket upgrade.

That's everything you need to know to ship jmaple. The topic guides go deeper on the subsystems you touched in the tutorial.