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>
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:
POST /auth/loginwith an email — server emails a one-time token.- The recipient clicks the link, which is
GET /auth/verify?token=.... - The verify endpoint returns a session JWT (24-hour lifetime).
- 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.