Files
librenotes/docs/api.md
Michael Czechowski 61edef9483 Add user guide, self-hosting, API, and contributing docs
docs/user-guide.md — magic-link sign-in, note basics, wikilink
syntax, keyboard shortcuts, offline behaviour, and privacy
notes (sessionStorage for tokens, tenant-scoped localStorage).

docs/self-hosting.md — system requirements, Docker Compose
quick-start, the full LIBRENOTES_* env-var matrix (which are
required, which conditional), reverse proxy snippets for Caddy
and nginx, volume layout, the in-binary healthcheck, and
update/rollback procedure.

docs/api.md — every public endpoint: auth (login/verify),
notes CRUD, /api/whoami, /healthz. Status codes per endpoint,
the optimistic-locking ?base=<unix> contract for PUT/DELETE,
note-ID regex, and the rate-limit policy.

CONTRIBUTING.md — dev setup (Nix flake .#dev, plain Go, Docker),
package layout overview, coding standards (one-way dep flow,
tenant FS gateway requirement), branch naming, commit format,
and the PR process. Also points security reports at
security@librete.ch rather than public issues.

Closes #29.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:52:09 +02:00

4.1 KiB

API reference

This is the public HTTP API of librenotes. All endpoints return JSON unless otherwise noted.

Base URL: whatever you configured as LIBRENOTES_BASE_URL (for the cloud: https://librenot.es).

Authentication

librenotes uses passwordless email magic links plus short-lived JWTs. The flow is:

  1. POST /auth/login with an email — server emails a one-time token.
  2. The recipient clicks the link, which is GET /auth/verify?token=....
  3. The verify endpoint returns a session JWT (24-hour lifetime).
  4. Subsequent calls send Authorization: Bearer <jwt>.

POST /auth/login

Request:

POST /auth/login
Content-Type: application/json

{ "email": "you@example.com" }

Response:

Status Meaning
202 Accepted Email accepted; magic link sent (or rate-limited silently).
400 Bad Request Body is malformed.
429 Too Many Requests Rate limit hit (5 per 15 min per email).

The response body is {"status":"sent"} on 202. The endpoint deliberately returns 202 even for unknown addresses to avoid account enumeration.

GET /auth/verify?token=<token> and POST /auth/verify?token=<token>

Consumes a magic-link token. Single-use. 15-minute expiry.

{
  "jwt":         "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
  "user_id":     "f7b2e2c3-4a51-4d7c-bdc0-...",
  "email":       "you@example.com",
  "expires_at":  1777411035
}
Status Meaning
200 OK Verified; JWT issued.
400 Bad Request token parameter missing.
401 Unauthorized Token unknown, expired, or already used.

Notes

All /api/* endpoints require Authorization: Bearer <jwt>. A 401 means the token is missing/invalid; the client should redirect to the login page.

GET /api/whoami

Returns the verified tenant identity.

{ "user_id": "f7b2e2c3-...", "email": "you@example.com" }

GET /api/notes

List all notes for the current tenant.

[
  { "id": "ideas",       "title": "Ideas",       "updated_at": 1777400000 },
  { "id": "shopping",    "title": "Shopping",    "updated_at": 1777300000 }
]

GET /api/notes/{id}

Read a single note.

{
  "id":         "ideas",
  "title":      "Ideas",
  "content":    "A few directions...\n",
  "updated_at": 1777400000
}
Status Meaning
200 OK Found.
400 Bad Request Invalid ID (must match ^[a-z0-9][a-z0-9_-]{0,127}$).
404 Not Found No such note for this tenant.

PUT /api/notes/{id}

Create or update a note. Supports optimistic locking via ?base=<unix-seconds>: the server checks the current updated_at against base and returns 409 if the note has been modified since the client last saw it.

Request:

PUT /api/notes/ideas?base=1777400000
Content-Type: application/json

{ "title": "Ideas", "content": "A few directions...\n" }
Status Meaning
200 OK Saved; body returns the new updated_at.
400 Bad Request Bad ID, missing fields, or invalid base.
409 Conflict Note has been modified after base; body returns the current note so the client can resolve.

DELETE /api/notes/{id}

Delete a note. Same ?base= semantics as PUT.

Status Meaning
204 No Content Deleted (or already gone).
409 Conflict Modified after base.

Health

GET /healthz

Unauthenticated. Returns {"status":"ok"} when the binary is serving requests. Used by load balancers and the Docker HEALTHCHECK.

Errors

Beyond the per-endpoint codes above, all endpoints may return 500 for unhandled internal errors. Bodies are plain text (internal server error), not JSON, to avoid leaking internals.

Rate limits

POST /auth/login is the only currently rate-limited endpoint: 5 requests per 15-minute window per email address. The window resets on the oldest attempt expiring.

Versioning

The current public API surface is unversioned. Breaking changes will be signalled via a major version tag in the project as a whole, with a deprecation window announced in CHANGELOG.md.