The Rust/Axum service publishes the authoritative OpenAPI 3.1 schema at /api/openapi.json, exported in the repository as api/openapi.json. Run npm run api:generate to regenerate that export and the types consumed by the Solid client; CI rejects drift. Browse the generated OpenAPI reference, or download the specification to generate clients for other languages. These routes cover the first metadata increment. All private church routes require an authenticated membership. Administrators and Editors can write; Viewers can read. Non-members receive 404 for church-scoped routes. Invalid request bodies, UUIDs, and query parameters receive 400. Bodies larger than 32 KiB receive 413. Responses and session credentials are not cached.
Sessions and workspaces🔗
| Method and path | Contract |
|---|---|
POST /api/v1/session | JSON { "email": "you@example.org", "password": "…" }; returns 204 with an HttpOnly, SameSite=Strict session cookie. |
DELETE /api/v1/session | Revokes the current session and clears its cookie; returns 204. |
GET /api/v1/churches | Returns { "churches": [...] }, containing only the user's memberships with id, name, timeZone, and role. |
GET /api/health/live | Returns { "status": "ok" } while the API runs. |
GET /api/health/ready | Returns 200 when storage is accessible or 503 with unavailable. |
Sessions expire after 12 hours. HTTPS origins use a Secure __Host-songcollect cookie; local HTTP uses songcollect. Every mutation, including login and logout, requires an Origin header equal to the configured APP_ORIGIN. The login limit is ten attempts per minute per connecting address; the general request limit is 120 per minute. Credentials never belong in URL parameters. Native clients currently retain the session cookie and send the configured origin themselves; native token authentication and cross-origin browser hosting remain planned.
Collection reads🔗
| Method and path | Response |
|---|---|
GET /api/v1/churches/{churchId}/songs | { "church": {...}, "songs": [...], "hasMore": false }. |
GET /api/v1/churches/{churchId}/songs/{songId} | { "song": {...} } with a strong quoted integer ETag. |
GET /api/v1/churches/{churchId}/songs/{songId}/revisions | { "revisions": [...] } ordered newest first, with id, version, metadata, and createdAt. |
List parameters are optional q (up to 200 characters) and offset (nonnegative integer, at most 100,000). Pages contain at most 50 songs. Search uses case/accent-normalized AND substring matching, escapes SQL wildcard characters, and searches title, alternate titles, authors, themes, and book/number pairs. Ordering is title then stable song ID. This is the initial metadata search, not the full release ranking algorithm.
Public list and single-song reads use /api/v1/public/churches/{churchId}/songs and /api/v1/public/churches/{churchId}/songs/{songId}. They expose only current unarchived metadata, song IDs/revision versions, and church name/time zone. There is no public history endpoint. A logged-in browser visiting a public URL still receives only the public projection.
Metadata commands🔗
POST /api/v1/churches/{churchId}/songs creates a song and returns 201, Location, ETag, and { "song": {...} }. PUT /api/v1/churches/{churchId}/songs/{songId} appends a metadata revision and returns 200 and ETag. Both commands require a UUID Idempotency-Key. An update additionally requires If-Match: "1", using the version being edited.
{
"title": "Amazing grace",
"alternateTitles": [],
"authors": ["John Newton"],
"copyright": "",
"themes": ["Grace"],
"songbooks": [{ "book": "Church hymnal", "number": "123" }]
}
Only title is required; omitted arrays and copyright default to empty. Title, alternate titles, authors, and book names have a 200-character maximum; theme names allow 80, book numbers 30, and copyright 1,000. Arrays allow 20 alternate titles and 30 authors, themes, or songbook references. Empty entries and unknown properties are rejected. Schema version 1 is stored with each immutable document.
Missing If-Match returns 428; weak, wildcard, or malformed versions return 400. A stale write returns 412 with { "error": "…", "current": { ... } }. Preserve the user's draft, compare it with current, then either reload or explicitly resubmit using that current version and a new operation key. A newer intervening save is checked again; there is no unconditional overwrite.
Repeat the exact operation with the same idempotency key after a lost response. A completed operation returns its original result, even if a later edit advanced the song. Changing the request body or precondition with that key returns 409. Keys are scoped to actor and church; failed transactions do not consume them. Records currently have no retention expiry.
Every save transaction advances the revision head and commits an audit event, church cursor, outbox entry, and operation result atomically. Events are retained but not delivered in this increment. Restoring revisions, archiving via API, lyrics/arrangements, church administration, and service commands remain planned.