API Reference
REST under /api, JSON in and out, validated with the zod schemas in shared/. Routes are assembled in core/src/app.ts; handlers live in core/src/routes/.
Authentication. Everything under /api except /api/health and /api/auth/* requires a valid session cookie (requireAuth). /api/admin/* additionally requires users.is_admin (requireAdmin). The /dav tree does not use sessions — it authenticates with HTTP Basic and an app password.
Scoping. Every handler scopes its queries by the authenticated users.id. There is no cross-user read path except the explicit sharing endpoints.
Errors. Unhandled failures return 500 {"error":"Internal server error"}; oversized bodies return 413. Validation failures return 400 with the zod issue.
Public
| Method | Path | Notes |
|---|---|---|
GET | /api/health | {"status":"ok"}. No auth. |
GET | /api/auth/config | What sign-in methods this instance offers, and whether registration is open. |
Authentication — /api/auth
| Method | Path | Notes |
|---|---|---|
POST | /api/auth/register | Open self-registration (unless the admin disabled it). The first account registered becomes the admin. Rate limited: 5/min per IP. |
POST | /api/auth/login | Username + password. If TOTP is enabled, returns a pending-login handle instead of a session. Rate limited: 10/min per IP. |
POST | /api/auth/login/totp | Second step: pending-login handle + TOTP code or recovery code. Rate limited: 10/min per IP. |
POST | /api/auth/logout | Clears the session and the cookie. |
POST | /api/auth/passkey/login/options | WebAuthn assertion options. |
POST | /api/auth/passkey/login/verify | Verify the assertion, mint a session. |
Account — /api/me
| Method | Path | Notes |
|---|---|---|
GET | /api/me | The current Identity — includes isAdmin. |
PATCH | /api/me | Update display name, email, password, discovery visibility. Rate limited: 10/min. |
DELETE | /api/me | Delete the account and everything it owns. |
GET PATCH | /api/me/settings | Notification toggle, default reminders, all-day reminder time, custom theme. |
POST | /api/me/avatar | Upload an avatar (stored as a blob in users). |
DELETE | /api/me/avatar | Remove it. |
GET | /api/avatars/:userId | Serve another user's avatar (for shared-calendar UI). |
Passkeys — /api/me/passkeys
| Method | Path |
|---|---|
GET | /api/me/passkeys |
POST | /api/me/passkeys/register/options |
POST | /api/me/passkeys/register/verify |
DELETE | /api/me/passkeys/:id |
TOTP — /api/me/totp
| Method | Path | Notes |
|---|---|---|
GET | /api/me/totp | Enrollment state. |
POST | /api/me/totp/setup | Generate a secret + provisioning URI. |
POST | /api/me/totp/enable | Confirm with a code; returns recovery codes once. |
POST | /api/me/totp/disable | |
POST | /api/me/totp/recovery/regenerate | Invalidates the previous set. |
App passwords — /api/me/app-passwords
Revocable Basic-auth credentials for CalDAV clients (DAVx5 and friends).
| Method | Path | Notes |
|---|---|---|
GET | /api/me/app-passwords | Metadata only — the token is never returned again. |
POST | /api/me/app-passwords | Returns the plaintext token once. |
DELETE | /api/me/app-passwords/:id |
Push — /api/me/push
| Method | Path | Notes |
|---|---|---|
GET | /api/me/push/key | The VAPID public key, or null if the server has no keys configured. |
POST | /api/me/push/subscribe | Register this device's endpoint. |
POST | /api/me/push/unsubscribe |
End-to-end encryption — /api/me/encryption
| Method | Path | Notes |
|---|---|---|
GET | /api/me/encryption | The wrapped-key bundle, verbatim. |
PUT | /api/me/encryption | Store a new bundle. The server never sees the data key. See Security Model. |
Calendars — /api/calendars
| Method | Path | Notes |
|---|---|---|
GET | /api/calendars | Owned calendars plus ones shared with you. |
POST | /api/calendars | |
PATCH | /api/calendars/:id | Name, color, description, timezone, default reminders, sort order, CalDAV exposure. |
DELETE | /api/calendars/:id | |
PATCH | /api/calendars/:id/visibility | Show/hide — for a shared calendar this sets hidden_by_grantee, not the owner's flag. |
GET | /api/calendars/:id/conversion | Preview converting a calendar to/from encrypted. |
POST | /api/calendars/:id/convert | Perform the conversion. |
GET | /api/calendars/:id/shares | |
POST | /api/calendars/:id/shares | Grant a role to another user. |
DELETE | /api/calendars/:id/shares/:granteeId |
Events — /api/events
Reading
| Method | Path | Notes |
|---|---|---|
GET | /api/events | Range query (from/to). Recurring masters are expanded and overrides applied. This is the hot path the week view uses, and the one the service worker caches network-first. |
GET | /api/events/:id | |
GET | /api/events/search | Backed by the event_search_tokens / override_search_tokens index tables. |
GET | /api/events/trash | Soft-deleted events (deleted_at set). |
GET | /api/events/invitations | Events you've been invited to. |
GET | /api/events/stream | Server-sent events — live updates for open clients. |
Writing
| Method | Path | Notes |
|---|---|---|
POST | /api/events | |
PATCH | /api/events/:id | Optimistic concurrency via the row's version. |
DELETE | /api/events/:id | Soft delete → trash. |
POST | /api/events/:id/restore | Undelete from trash. |
POST | /api/events/sync | Batch replay of the offline queue; returns per-item results and conflicts. |
Recurrence
| Method | Path | Notes |
|---|---|---|
POST | /api/events/:id/override | Edit or cancel a single occurrence. |
POST | /api/events/:id/split | "This and all future" — ends the master and starts a new series. |
Sharing and attendees
| Method | Path |
|---|---|
GET POST | /api/events/:id/shares |
DELETE | /api/events/:id/shares/:granteeId |
GET POST | /api/events/:id/attendees |
DELETE | /api/events/:id/attendees/:userId |
POST | /api/events/:id/rsvp |
Attachments
| Method | Path | Notes |
|---|---|---|
POST | /api/events/:id/attachments | Multipart. Enforces the admin-configured per-file limit under the global body cap. |
GET | /api/events/:id/attachments/:attId | Served with nosniff and a Content-Disposition. |
DELETE | /api/events/:id/attachments/:attId |
Tasks — /api/tasks
| Method | Path |
|---|---|
GET POST | /api/tasks |
PATCH DELETE | /api/tasks/:id |
Collaboration — /api
| Method | Path | Notes |
|---|---|---|
GET | /api/users | Directory for invites. Users who set discovery_disabled are excluded. |
GET | /api/freebusy | Opaque busy blocks for the given users and range — no titles, no details. Backs the "find a time" overlay. |
Import / export — /api
| Method | Path | Notes |
|---|---|---|
GET | /api/export.ics | Standard iCalendar. Encrypted calendars are excluded — the server only holds ciphertext. |
POST | /api/import | Import an .ics file. |
GET | /api/export.hypercalendar | Native JSON format. Preserves what iCalendar can't carry: per-event color, reminders, calendar grouping, per-occurrence override colors. |
POST | /api/import.hypercalendar |
Subscriptions — /api/subscriptions
Read-only external ICS feeds. A subscription is a calendar row with subscription_url set, so listing, editing, and deleting go through /api/calendars.
| Method | Path | Notes |
|---|---|---|
POST | /api/subscriptions | Add a feed. The URL is checked against the SSRF guard (core/src/lib/ssrf.ts) before fetching. |
POST | /api/subscriptions/:id/refresh | Force a refresh now. |
Weather — /api/weather
Hourly forecast and severe-weather warnings for the week-view lens, proxied rather than fetched from the browser: the app's CSP is connect-src 'self', so the page cannot reach a third-party host, and going through the server keeps the user's IP away from the weather source. The outbound call uses the same safeFetch guard as subscriptions, which the standalone Android build swaps for its native-HTTP shim.
| Method | Path | Notes |
|---|---|---|
GET | /api/weather?lat=&lon=&from=&to= | from/to are unix ms, half-open, at most 11 days wide. |
Responses are cached in memory for 30 minutes (forecast) and 5 minutes (warnings), keyed by provider, coordinates rounded to two decimals, and the window snapped to whole UTC days. A source failure is reported as 502 with a generic message; the upstream body is never echoed. An empty hours array means the source has no data for that window, which is what it returns past its forecast horizon, and is not an error.
Only Bright Sky (Deutscher Wetterdienst, Germany) is implemented. Set WEATHER_BRIGHTSKY_URL to point at a self-hosted instance; that is deliberately server configuration rather than a client setting, so an authenticated user cannot name an arbitrary URL for the server to fetch.
CalDAV links — /api/caldav
Two-way sync to a remote CalDAV collection (Nextcloud, Radicale, Fastmail, …). Requires CALDAV_SECRET to be set; without it, linking is refused rather than storing a password in the clear.
| Method | Path | Notes |
|---|---|---|
POST | /api/caldav/links | Link a calendar to a remote collection URL. Point it at a single calendar, not the server root — there is no principal discovery. |
PATCH | /api/caldav/links/:id | |
DELETE | /api/caldav/links/:id | |
POST | /api/caldav/links/:id/sync | Sync now. |
The stored password is never returned by any response, export, or backup — there's a regression test for exactly that (caldav/links.test.ts).
Backups — /api/backups
Per-user .ics snapshots. Requires a host with filesystem access; on the Android build these report the feature as unavailable.
| Method | Path |
|---|---|
GET | /api/backups |
POST | /api/backups |
GET | /api/backups/:date |
POST | /api/backups/:date/restore |
Admin — /api/admin
Requires users.is_admin.
| Method | Path | Notes |
|---|---|---|
GET | /api/admin/settings | |
PATCH | /api/admin/settings | Notably: close registration once the household's accounts exist. |
GET | /api/admin/db-backups | List database snapshots. |
POST | /api/admin/db-backups | Take one now (VACUUM INTO). |
GET | /api/admin/db-backups/:name | Download one. |
CalDAV server — /dav
Lets DAVx5, Thunderbird, Apple Calendar, etc. read and write hypercal directly. HTTP Basic auth with an app password — not the session cookie, and not your account password.
OPTIONS,PROPFIND,REPORT,GET,PUT,DELETEon the collection and its objects./.well-known/caldav→301to/dav/so clients can discover the service from the bare domain.- Trailing slashes are optional (the router runs
strict: false).
Only calendars with caldav_enabled are exposed. Encrypted calendars never are.