Jmaple
A pluggable JMAP server framework, built on FastAPI.
📖 Documentation — tutorial, topic guides, and API reference.
JMAP (RFC 8620) was designed as a generic JSON-over-HTTP sync protocol with a clean separation between core machinery (sessions, request/response envelope, method dispatch, references, state strings, push) and data-type capabilities (Mail = RFC 8621, Contacts = RFC 9610, …). Jmaple takes that separation seriously and treats every data type — including Mail — as a plugin.
- Python: 3.14+
- Package manager: uv
- Web framework: FastAPI
- ORM: SQLAlchemy (async)
- Migrations: Alembic (dynamically discovers plugin migrations via Python entry points)
- Operator interface: a single
jmapleCLI — there is no separate admin web UI.
What's in the box
- Full JMAP Core (RFC 8620): session resource, method dispatch, result references, state strings, error handling, upload/download.
- A clean plugin contract so a third party can add a new capability
(
urn:vendor:thing) by registering schemas and method handlers, without touching the core. - A pluggable auth subsystem (JWT, OIDC, opaque bearer) wired into a unified accounts/grants authorization layer stored in the database.
- A pluggable persistence layer built on async SQLAlchemy 2.0 + Alembic, with PostgreSQL as the default and SQLite for development.
- All three JMAP push transports: SSE (
/eventsource), WebSocket (RFC 8887), and PushSubscription webhooks (RFC 8620 §7.2). - A reference plugin (
urn:jmaple:notes) demonstrating end-to-end use of every abstraction.
Quickstart
You need uv and just on
your $PATH.
just install # uv sync
cp .env.example .env # tweak JMAPLE_DATABASE_URL, JMAPLE_SECRET_KEY, …
just migrate # apply Alembic migrations
just serve # uvicorn at http://localhost:8000
# In another terminal: issue the first admin token.
uv run jmaple token issue --user alice --admin
Then point a JMAP client at http://localhost:8000/.well-known/jmap with the
issued token in the Authorization: Bearer <token> header.
Top-level recipes
| Recipe | Description |
|---|---|
just install |
uv sync. |
just serve |
Run uvicorn with --reload. |
just migrate |
jmaple migrate upgrade head. |
just revision "msg" |
jmaple migrate revision --autogenerate -m "msg". |
just fmt / just lint / just type / just test |
Ruff format / Ruff lint / ty check / pytest. |
just check |
All four of the above. |
just package |
uv build — produces wheel + sdist in dist/. |
The jmaple CLI
The CLI is the only operator interface. Run uv run jmaple --help for the full
tree. Top-level groups:
jmaple serve— start the FastAPI server.jmaple migrate {upgrade, downgrade, revision, history, current}— Alembic.jmaple user {list, show, create, promote}— manage users.jmaple account {list, create, rename, delete}— manage accounts.jmaple grant {list, add, remove}— manage account-sharing grants.jmaple token {issue, list, revoke}— manage opaque bearer tokens.jmaple subscription {list, revoke}— inspect / revoke push subscriptions.jmaple capability {list, settings get, settings set}— inspect / configure registered capabilities.jmaple version— print the installed version.
Public API
When writing a capability, import from these modules. Everything under
jmaple.core.*, jmaple.auth.*, and jmaple.api.* is internal and may change
without notice.
| Module | What's in it |
|---|---|
jmaple.capabilities |
Capability, CrudCapability, BaseCapability, MethodHandler, DataType, make_capability, register_capability |
jmaple.db |
Base, JMAPObject, DataChange, UTCDateTime, new_id, utcnow, id_column, created_column |
jmaple.schemas |
JMAPGetArgs, JMAPGetResponse, JMAPSetArgs, JMAPSetResponse, JMAPQueryArgs, JMAPQueryResponse, JMAPChangesArgs, JMAPChangesResponse, JMAPCopyArgs, JMAPCopyResponse, JMAPQueryChangesArgs, JMAPSort |
jmaple.types |
Id, UTCDate, ResultReference, PatchObject |
jmaple.context |
MethodContext |
jmaple.errors |
MethodError, SetError, all ERR_* constants |
A minimal capability looks like:
# my_plugin/models.py
from jmaple.db import Base, JMAPObject
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
class Todo(Base, JMAPObject):
__tablename__ = "todos"
text: Mapped[str] = mapped_column(String(512))
# my_plugin/capability.py
from jmaple.capabilities import CrudCapability
from my_plugin.models import Todo
from my_plugin.schema import TodoSchema, TodoCreate
class TodosCapability(CrudCapability):
urn = "urn:example:todos"
data_type_name = "Todo"
model = Todo
schema_class = TodoSchema
create_schema_class = TodoCreate
capability = TodosCapability.as_capability()
Register it programmatically in your ASGI app module:
# my_plugin/app.py
from jmaple.app import create_app
from jmaple.capabilities import register_capability
from my_plugin import models as _models # noqa: F401 # land Todo on Base.metadata
from my_plugin.capability import capability
register_capability(capability)
app = create_app()
Run with uvicorn my_plugin.app:app. For CLI ops, write a parallel
my_plugin/cli.py that registers the capability then re-exposes
jmaple.cli:app, and point pyproject.toml's jmaple script at it.
Environment
The server reads its config from .env in the working directory (via
pydantic-settings). Copy .env.example and adjust as needed:
JMAPLE_DATABASE_URL— SQLAlchemy async URL (defaults to a local SQLite file).JMAPLE_SECRET_KEY— random string for state-vector / session signing.JMAPLE_DEBUG—truefor verbose logs.
Operators of the published PyPI wheel set the same JMAPLE_* env at deploy time.
Release files for jmaple 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jmaple-0.1.0.tar.gz | 80.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jmaple-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 168.7 kB
Release files / jmaple-0.1.0.tar.gz
| Download URL | jmaple-0.1.0.tar.gz |
|---|---|
| Size | 80.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1ef0f379cc5fdbc24cd8800fc73a0447e73edd2940ccdff1fa598935f02307a9
|
|
BLAKE2b-256 checksum How to use checksums |
df0b73d57686914f38faf9a8cd43849a6c4f3ac970560db646219ae646fadf4e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.15 {"installer":{"name":"uv","version":"0.11.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / jmaple-0.1.0-py3-none-any.whl
| Download URL | jmaple-0.1.0-py3-none-any.whl |
|---|---|
| Size | 87.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ecd0130bc2e57bef8383b04eaf7e756876bb9c45e9492ed70739a4ddc6c59093
|
|
BLAKE2b-256 checksum How to use checksums |
74c51317d1af09a902e433dd77b8c1743747bfb48fd2a73ddf520465ba64926d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.15 {"installer":{"name":"uv","version":"0.11.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|