2026-08-19 17:18:45 +01:00
2026-08-19 17:18:45 +01:00
2026-08-19 17:18:45 +01:00
2026-08-19 17:18:45 +01:00
2026-08-19 17:18:45 +01:00
2026-08-19 17:18:45 +01:00
2026-08-19 17:18:45 +01:00
2026-08-19 17:18:45 +01:00
2026-08-19 17:18:45 +01:00
2026-08-19 17:18:45 +01:00
2026-08-19 17:18:45 +01:00
2026-08-19 17:18:45 +01:00
2026-08-19 17:18:45 +01:00
2026-08-19 17:18:45 +01:00

joplin-cli-gateway logo

joplin-cli-gateway

A permission-filtered REST and GraphQL API over one managed Joplin CLI profile. The gateway keeps the profile synchronized with an existing Joplin Server and allows multiple machine clients to use ordinary Joplin notes without installing Joplin themselves.

The product intent is in speccs.md, the public API draft is in api-mvp.md, and the stack decision is in language-framework-analysis.md.

Implementation

The current implementation provides:

  • TypeScript on Node.js 22;
  • Fastify REST endpoints under /api/v1;
  • Mercurius GraphQL at /graphql;
  • OAuth-style client-credentials token issuance at /oauth/token;
  • administrator-owned JSON client configuration;
  • scrypt-hashed or development plaintext client secrets;
  • durable JSON state for automatic grants and pending synchronization operations;
  • inherited notebook read/write permissions and path-only ancestors;
  • notes, notebooks, tags, trash, revision reconstruction/restoration, search, and change polling;
  • complete hiding of to-dos, conflict notes, and inaccessible content;
  • one serialized owner for synchronization and profile operations;
  • hard failure when pre-operation sync fails;
  • 202/pending status when a local mutation succeeds but post-operation sync fails;
  • one pre-sync and at most one post-sync for an entire GraphQL document.

The Joplin adapter deliberately uses a process boundary. It stops the local Data API server, runs joplin sync, starts the server for local operations, and stops it again before the next sync. The Data API process and a sync process therefore never access the profile concurrently.

Joplin labels its CLI Data API server experimental. Pin the Joplin version and repeat a real-profile compatibility check against that version before treating an upgrade as safe.

Requirements

  • Node.js 22 or later for source operation; or Docker;
  • Joplin Terminal installed as joplin;
  • a dedicated persistent Joplin CLI profile;
  • that profile configured for Joplin Server sync target 9 (or Joplin Server SAML target 11);
  • an HTTPS Joplin Server URL.

The gateway refuses to start when the profile uses a non-Joplin-Server target or a non-HTTPS server URL.

Configure a profile

Use a profile dedicated to the gateway. Do not open it from a separate Joplin process.

Typical Joplin Server settings are:

joplin --profile /path/to/gateway-profile config sync.target 9
joplin --profile /path/to/gateway-profile config sync.9.path https://joplin.example.com
joplin --profile /path/to/gateway-profile config sync.9.username you@example.com
joplin --profile /path/to/gateway-profile config sync.9.password 'server-password'
joplin --profile /path/to/gateway-profile sync

For Docker, run the same commands using the image and its /profile volume before starting the service.

Configure clients

Copy config/clients.example.json to config/clients.json. Set the profile path, a random local Data API token, client identities, and permissions.

For production client credentials, generate an encoded secret:

npm run hash-secret -- 'a-long-random-client-secret'

Store the result as client_secret_scrypt and remove client_secret. Plaintext secrets remain supported for initial development. Restrict the configuration file to the service operator because it also contains the local Data API token.

Configuration is loaded once. Restart the process after changing clients or permissions. Tokens use a restart-scoped signing key, so every restart invalidates all previously issued tokens; disabling or removing a client therefore blocks its next request.

Top-level notebooks created through the gateway produce automatic write grants in the gateway state file. To revoke one, stop the gateway, remove the corresponding grant from automatic_grants, and restart it. The state file is gateway-managed while the process is running.

Run from source

npm ci
cp config/clients.example.json config/clients.json
JCG_CONFIG_PATH=./config/clients.json npm run dev

Production build:

npm run check
npm test
npm run build
JCG_CONFIG_PATH=./config/clients.json npm start

Run with Docker

cp config/clients.example.json config/clients.json
mkdir -p state
docker compose -f docker-compose.example.yml up --build

The example publishes the gateway only on loopback. Put it behind an HTTPS reverse proxy for remote clients. Set server.trust_proxy only when the proxy is trusted and strips untrusted forwarding headers.

Obtain and use a token

curl -sS -X POST http://127.0.0.1:8080/oauth/token \
  -H 'content-type: application/x-www-form-urlencoded' \
  --data-urlencode grant_type=client_credentials \
  --data-urlencode client_id=example-reader \
  --data-urlencode client_secret='the-client-secret'

Then send the returned token:

curl -sS http://127.0.0.1:8080/api/v1/notebooks \
  -H 'authorization: Bearer ACCESS_TOKEN'

The liveness endpoint is GET /health/live. It deliberately does not access or synchronize Joplin.

Synchronization semantics

Every content operation is queued as one unit:

  1. stop the local Data API owner if running;
  2. run and await joplin sync;
  3. start the local Data API server;
  4. refresh the notebook hierarchy and permissions;
  5. perform the local operation;
  6. for a mutation, stop the Data API server and run joplin sync again.

A pre-sync failure returns 503 SYNC_UNAVAILABLE without reading local content. A post-sync failure preserves the local mutation and returns a pending sync result with an operation ID. The idle synchronization timer retries delivery.

Development checks

npm run check
npm test
npm run build

The automated suite uses an in-memory adapter and verifies authentication, non-disclosure, hierarchy permissions, mandatory sync behavior, degraded mutation reporting, GraphQL batching, and exclusive profile serialization. A real-profile compatibility suite remains necessary for each supported Joplin CLI version because the local Data API server is experimental.

S
Description
Permission-filtered REST and GraphQL gateway for Joplin CLI
Readme
131 KiB
Languages
TypeScript 99.3%
Dockerfile 0.7%