A pluggable JMAP server framework built on FastAPI.
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jmaple-0.1.0.tar.gz.
File metadata
- Download URL: jmaple-0.1.0.tar.gz
- Upload date:
- Size: 80.9 kB
- Tags: Source
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1ef0f379cc5fdbc24cd8800fc73a0447e73edd2940ccdff1fa598935f02307a9
|
|
| MD5 |
62db9c3bc3a26ac90ccbd5e6454a5cca
|
|
| BLAKE2b-256 |
df0b73d57686914f38faf9a8cd43849a6c4f3ac970560db646219ae646fadf4e
|
File details
Details for the file jmaple-0.1.0-py3-none-any.whl.
File metadata
- Download URL: jmaple-0.1.0-py3-none-any.whl
- Upload date:
- Size: 87.8 kB
- Tags: Python 3
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ecd0130bc2e57bef8383b04eaf7e756876bb9c45e9492ed70739a4ddc6c59093
|
|
| MD5 |
aaa639b08bb2c7a145e83c1a66873748
|
|
| BLAKE2b-256 |
74c51317d1af09a902e433dd77b8c1743747bfb48fd2a73ddf520465ba64926d
|