# Calendar iCal export and subscriptions How `GET /calendar/events/{calendar}/ical` builds its file, what changed on 2026-09-28, and the endpoint the calendar app uses to hand editors ready-made subscription URLs. ## The export `icalgenerator.service.ts` turns a calendar's `PUBLIC` events into one `VCALENDAR`: - Timed events: `DTSTART;TZID=Europe/Berlin:` / `DTEND;TZID=Europe/Berlin:` formatted with the **API process's local** `getHours()` etc. The times are therefore only right if the process runs in Berlin time (`TZ=Europe/Berlin`, or a server in that zone). Nothing enforces this. Inferred from the code; not checked against the production server. - Whole-day events: `VALUE=DATE`, and the stored end date is treated as the *inclusive* last day - the generator adds one day for `DTEND`. The calendar app stores whole-day events as 00:00 to 23:59 Berlin time for this reason. - `repeatFrequency` is written verbatim as `RRULE:FREQ=` (no interval, count or end). - `public` needs no credential; the other calendars take `?password=` because a calendar client cannot send the session cookie (see `calendar-auth-migration.md`). ## RFC 5545 conformance (2026-09-28) Before this date the generator wrote text values raw. A line break in a description ended the `DESCRIPTION` property early and the rest was read as a garbage property; unescaped `,` and `;` are invalid in text values. The new calendar frontend has a multi-line description field, so this went from theoretical to certain. Now: - `SUMMARY`, `DESCRIPTION` and `LOCATION` go through `escapeText` (`\` `;` `,` and line breaks, backslash first). - `URL` is a URI value and is not escaped; line breaks are stripped from it. - Every content line is folded at 75 octets (UTF-8 aware - a multi-byte character is never split) and the file uses CRLF line endings throughout (`toContentLines`). - `formatDate` no longer shifts the caller's `Date` when it adds the whole-day end's extra day. - The response is `Content-Type: text/calendar; charset=utf-8`. Before, Express inferred `text/html` from the string body; calendar apps coped, but it was wrong. Pinned by `test/calendar/icalgenerator.service.test.ts`. Not changed, worth knowing: `ORGANIZER` is written as a bare display name (`ORGANIZER:Anna`), while RFC 5545 expects a cal-address (`ORGANIZER;CN=Anna:mailto:…`). Calendar apps have tolerated it so far. ## `GET /calendar/events/subscriptions` For the calendar app's "Abonnieren" dialog. Behind `requireAppAccess('calendar')`; the shared password is **not** accepted here (one calendar's password must not reveal the others). Returns `[{calendar, icalUrl}]` in `calendarNames` order, built from `API_BASE_URL`: - `public` without a password; - every other calendar with `?password=`, URL-encoded; - a calendar whose credential env var is unset is left out rather than listed with a URL that cannot work. `choir` and `birthdays` share `CHOIR_CREDENTIAL`, so rotating it changes both URLs - and breaks every existing subscription to either. The response is sent with `Cache-Control: no-store` because it carries the passwords. Signed-in editors can read every event of every calendar anyway, so handing them the passwords exposes nothing they could not already see. ## Found while doing this - Only the admin pool reads `DB_PORT`; `Calendar.db.ts` (and the feedback/tickets pools) always connect to 3306. A local test database for the calendar has to listen there.