Skip to content

Apple / iCloud and CalDAV

The adapter at apps/api/src/sync/adapters/caldav.ts serves Apple/iCloud, Nextcloud, Radicale, Fastmail, and other CalDAV servers. It synchronizes VEVENT and VTODO collections in both directions, supports multiple accounts, and uses Basic authentication rather than OAuth.

  1. Configure credential encryption.

    Terminal window
    openssl rand -hex 32

    Put the result in the API environment:

    CALDAV_ENC_KEY=<64-hex-characters>

    Changing this key without migrating stored ciphertext makes existing CalDAV credentials and federated member tokens unreadable.

  2. Collect server credentials.

    You need a CalDAV server URL, username, and password. Outside development, the URL must use HTTPS and every discovered or redirected target is checked against the private-address SSRF guard before credentials are sent. iCloud requires an Apple app-specific password, never the Apple ID password.

  3. Connect through Settings.

    Credentials are stored in caldav_accounts, separate from Better Auth’s OAuth account table. Passwords are AES-GCM encrypted before persistence.

  4. Inspect discovery and the first pull.

    Use LOG_LEVEL=debug temporarily and correlate failures by requestId. Verify calendar discovery before diagnosing individual event mapping.

providerFlavor() labels an icloud.com server as Apple for display. The underlying sync path remains CalDAV.

When supported, the first pull uses WebDAV sync-collection without a token to fetch the complete collection and obtain its cursor. Later pulls fetch only changed resource URLs; deleted URLs become tombstones for both resource kinds so the existing mapping is removed safely.

An expired token, unsupported report, malformed response, or missing next token falls back to a complete depth-one PROPFIND listing and calendar multiget. This avoids iCloud’s calendar-query time-range requirement while keeping reset sweeps complete. Unchanged ETags remain verified no-ops.

CalDAV/iCalendar shape Musubi behavior
Resource URL Stable externalId; UID alone is not used
DATE DTSTART All-day event at UTC midnight
Exclusive DTEND Subtract one day for inclusive Musubi end
RRULE, EXDATE, RDATE, detached instances Preserve and project recurrence exceptions
TZID + VTIMEZONE Resolve instants and preserve the original zone on update
VALARM Preserve alarm components while editing event or task fields
VTODO Import and write task fields, UID, and ETag
ETag unchanged No database write and no updatedAt bump

Updates patch the existing calendar resource rather than replacing its complete iCalendar payload. This keeps server-owned fields, detached recurrence components, timezones, and alarms intact. If the recurrence rule itself changes, stale detached instances are removed with the old series.

Event objects use the typed tsdav client. Calendar lifecycle operations use raw WebDAV so Musubi controls XML and Apple properties:

  • MKCALENDAR for creation;
  • PROPPATCH for display name and Apple calendar color; and
  • DELETE for removal.

The adapter derives the calendar-home URL from an existing calendar. A 207 Multi-Status response to PROPPATCH is successful; a server that ignores Apple’s color property is non-fatal.

Generate a fresh app-specific password after confirming two-factor authentication is enabled. Do not enter the normal Apple ID password.

Check the exact collection URL and whether the server permits depth-one PROPFIND, calendar-multiget, or sync-collection on that collection.

Inspect ETag preservation and reset-upsert behavior. A quiet, unchanged full-fetch should not bump local timestamps or emit an external-sync nudge.

Credentials became unreadable after deployment

Section titled “Credentials became unreadable after deployment”

Verify the deployment still has the original CALDAV_ENC_KEY. Restore that key from the secret manager; guessing or regenerating it cannot decrypt existing rows.

  • test against a generic CalDAV server and iCloud when feasible;
  • preserve resource URLs, ETags, inclusive/exclusive dates, and DATE-typed exceptions;
  • cover 207 Multi-Status and ignored color-property behavior;
  • never log Basic auth values, encrypted payloads, tokens, or full request bodies;
  • verify incremental pulls and expired-token fallback; and
  • run pnpm check plus a disposable-account round trip.