Calino syncs user settings (theme, first day of week, time format, etc.) across devices using a non-standard CalDAV approach. This document explains exactly how it works, why it was designed this way, and what to expect.
CalDAV is designed for calendar data (VEVENT, VTODO, VJOURNAL). There is no standard way to sync arbitrary JSON settings via CalDAV. Options considered:
| Approach | Problem |
|---|---|
| Store JSON file via WebDAV PUT | CalDAV calendar collections reject non-iCalendar files (405 Method Not Allowed on Baikal/SabreDAV) |
| Store in a VEVENT DESCRIPTION field | iCalendar 75-octet line folding breaks long base64 payloads |
| Store in a VTODO | Pollutes task lists, confusing users |
| Create a separate WebDAV collection | Not supported by all CalDAV servers; requires non-standard PROPFIND |
Chosen approach: A dedicated calendar collection containing a single VEVENT with the settings payload encoded as base64 in the ATTACH property.
calDAV Server
└── calino-settings/ ← Dedicated calendar collection
└── calino-settings.ics ← Single VEVENT file
- Display name:
Calino Settings - Internal name:
calino-settings - Components: VEVENT only (no VTODO)
- Hidden from UI: Filtered out of Calino's sidebar unless CalDAV debug mode is enabled
A single VEVENT with a fixed UID lives inside the collection:
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Calino//Settings Sync//EN
BEGIN:VEVENT
UID:00000000-calino-0000-calino-000000000000
DTSTAMP:20260609T185530Z
DTSTART:19700101T000000Z
DTEND:19700101T000001Z
SUMMARY:Calino Settings
TRANSP:TRANSPARENT
CLASS:PRIVATE
X-CALINO-VERSION:1
ATTACH;ENCODING=BASE64;FMTTYPE=app/json:<base64-encoded JSON>
END:VEVENT
END:VCALENDAR| Field | Purpose |
|---|---|
UID |
Fixed UUID shared across all Calino instances — ensures exactly one settings event exists |
DTSTART |
Unix epoch (1970-01-01) — keeps the event out of every real calendar view |
TRANSP:TRANSPARENT |
Never blocks time in any calendar view |
CLASS:PRIVATE |
Hidden in shared calendar contexts |
X-CALINO-VERSION |
Schema version for future migration support |
ATTACH;ENCODING=BASE64;FMTTYPE=app/json |
The actual settings payload |
The ATTACH field contains base64-encoded JSON:
{
"version": 1,
"syncedAt": "2026-06-09T18:55:30.755Z",
"settings": {
"timezone": "Europe/Copenhagen",
"dateFormat": "dd/MM/yyyy",
"timeFormat": "12h",
"firstDayOfWeek": 1,
"defaultDuration": 60,
"defaultView": "month",
"showWeekNumbers": true,
"eventDensity": "comfortable",
"defaultReminderMinutes": 15,
"defaultEventColor": "#4285F4",
"enableDesktopNotifications": true,
"enableSoundAlerts": false,
"compactRecurringEvents": true,
"compressPastWeeks": true,
"monthViewEventLimit": 3,
"themeMode": "auto",
"lightTheme": "default-light",
"darkTheme": "default-dark",
"hideCompletedTasksInMonthView": true,
"useCategoryColors": true,
"journalEnabled": false
}
}Synced (UI preferences):
- Timezone, date/time format, first day of week
- Default view, event density, week numbers
- Theme mode, light/dark theme names
- Notification preferences
- Category colors, completed task visibility
NOT synced (local-only):
- CalDAV account credentials
- Sync configuration (enabled, interval, conflict resolution)
- Onboarding state
- Sidebar width/collapsed state
- Debug mode
Uses DTSTAMP from the remote VEVENT vs. lastSyncedAt from localStorage.
Remote DTSTAMP > lastSyncedAt → Remote wins (server was updated since our last pull)
Remote DTSTAMP ≤ lastSyncedAt → Local wins (we're up to date or ahead)
| Scenario | Result |
|---|---|
| Change setting on Device A, push | Device A's lastSyncedAt updated |
| Device B syncs | Remote DTSTAMP > B's lastSyncedAt → B gets A's changes |
| Device B changes setting locally | B's lastSyncedAt unchanged (only set on pull) |
| Device B syncs again | If no remote change, local wins; if remote changed, remote wins |
Key: lastSyncedAt is only updated after a successful pull, not after a push. This ensures remote changes always win if they happened after our last sync.
Happens as part of the normal CalDAV calendar sync cycle:
- Calendar sync completes
- Check if
primaryAccountIdmatches this account - Discover settings calendar via PROPFIND
- Fetch settings VEVENT via REPORT (UID filter)
- Extract base64 from ATTACH, decode, parse JSON
- Compare
DTSTAMPvslastSyncedAt - If remote wins → merge settings into Zustand store
- Update
lastSyncedAt
User clicks "Push" button in Settings → Sync:
- Serialize current settings to JSON
- Base64 encode
- Fetch existing VEVENT to get current ETag
- PUT updated VEVENT with
If-Match: <server-etag> - Store new ETag
When a CalDAV account is added:
- After calendars are fetched, check for
Calino Settingscalendar - If found and no
primaryAccountIdset → auto-enable sync - Pull settings from the discovered calendar
- Show toast: "Calino Settings found — sync enabled automatically."
- CalDAV uses ETags for optimistic locking
- Each PUT includes
If-Match: <etag>to prevent lost updates - If ETag mismatches (412), the client fetches the latest and retries
- Server's ETag always takes priority over locally stored ETag
iCalendar lines are folded at 75 octets. Continuation lines start with a space or tab:
ATTACH;ENCODING=BASE64;FMTTYPE=app/json:ewogICJ2ZXJzaW9uIjogMSwK
ICAic3luY2VkQXQiOiAiMjAyNi0wNi0wOVQxODo1NTozMC43NTVaIiwKCalino unfolds these before extracting the base64 payload.
CalDAV servers may return relative hrefs in PROPFIND/REPORT responses (e.g., /dav.php/calendars/user/calino-settings/). These are resolved against the real server origin (from account.serverUrl), not the page origin.
- Single settings payload — No per-category or per-calendar settings sync
- No real-time sync — Settings are only pulled during calendar sync cycles
- Manual push — Users must click "Push" to send changes (no auto-push)
- One event per calendar — The fixed UID ensures exactly one settings event
- Server compatibility — Requires CalDAV servers that support REPORT queries with UID filters (Baikal, Nextcloud, Radicale, etc.)
| Key | Purpose |
|---|---|
calino.settingsSync.primaryAccountId |
Account ID used for sync (presence = sync active) |
calino.settingsSync.etag |
ETag of the last-synced settings VEVENT |
calino.settingsSync.lastSyncedAt |
ISO timestamp of last successful pull |
| File | Purpose |
|---|---|
src/lib/settingsSync.ts |
Serialization, deserialization, merge, conflict resolution, localStorage helpers |
src/hooks/useSettingsSync.ts |
React hook: enable/disable/push/pull/discover |
src/features/caldav/client/CalDAVClient.ts |
CalDAV operations: discover, fetch, put, delete settings calendar/VEVENT |
src/features/caldav/hooks/useCalDAV.ts |
Auto-discovery on account add, settings pull after calendar sync |
src/features/settings/components/GeneralSettings.tsx |
UI: sync section, account picker, error messages |