Files
familysync/.planning/research/PITFALLS.md
T

41 KiB

Pitfalls Research

Domain: Fastmail-brokered family calendar + shared-list PWA with Web Push, self-hosted behind Authelia Researched: 2026-06-03 Confidence: MEDIUM-HIGH (CalDAV/RRULE/iOS push well-documented in community; Fastmail-specific rate limits and JMAP calendar status confirmed from official docs)


Critical Pitfalls

Pitfall 1: Fastmail Calendars Are CalDAV-Only — JMAP Calendar Is Not Production-Ready

What goes wrong: Fastmail's developer documentation explicitly states: "Calendars — you can access via CalDAV. We will be opening up JMAP access as well, as soon as the specification is finalized." If you build your calendar broker against JMAP expecting calendar read/write, you will find either no endpoint or an unstable/draft surface. This kills the entire broker strategy.

Why it happens: JMAP for Calendars (RFC draft) has been "almost finalized" for years. It's easy to assume Fastmail's JMAP support is comprehensive because their email/contacts JMAP support is excellent. Calendar is the exception.

How to avoid: Commit to CalDAV as the protocol for all calendar read/write. Do not design the broker around JMAP calendars or leave it open. Use https://caldav.fastmail.com/dav/principals/user/{email}/ as the base URL — the bare caldav.fastmail.com root is not sufficient for library URL discovery.

Warning signs:

  • Any design doc that says "CalDAV or JMAP, TBD"
  • Libraries that prefer JMAP and fall back silently

Phase to address: Calendar broker implementation phase (first phase that touches Fastmail). Lock the protocol decision in the first spike; do not re-evaluate.


Pitfall 2: Recurring Event RRULE Expansion Done in the App Instead of Leveraged from Server

What goes wrong: The app fetches VCALENDAR objects and attempts to expand RRULE recurrences in application code using a generic JS library (e.g., rrule.js). The RFC 5545 recurrence model is large: RRULE, RDATE, EXDATE, and RECURRENCE-ID overrides interact. Bugs appear in: yearly events near leap-day, monthly BYDAY rules (e.g., "last Friday"), weekly events near DST transitions, and any series with moved/cancelled individual instances (RECURRENCE-ID).

Why it happens: Developers underestimate RFC 5545 complexity. Square built an internal RRULE library because existing ones couldn't handle the full spec. Even rrule.js has known edge cases with DST and complex rules. The CalDAV spec provides server-side expansion (CALDAV:expand) exactly because client expansion is error-prone.

How to avoid: Use CalDAV's server-side expansion. Issue time-ranged REPORT requests with <C:expand> to get back already-expanded instances within a window, rather than fetching raw VCALENDAR and expanding yourself. Only expand locally for display rendering (simple cases). Use rrule.js only for the display layer on events you've already confirmed against server expansion.

Warning signs:

  • Events that appear correct in simple cases but shift by one hour across DST boundaries
  • A recurring weekly event showing on the wrong day for specific months
  • Cancelled or moved instances reappearing

Phase to address: Calendar fetch/display phase. Define the REPORT request format in the first calendar sync spike, not as a later optimization.


Pitfall 3: All-Day Events Interpreted as Timed UTC Events

What goes wrong: All-day events in iCalendar use DATE (not DATETIME) values and carry no timezone. If your backend stores or returns them as UTC datetimes, or if the frontend uses .toISOString() on the date, an all-day event for "June 5" becomes "June 4 at 20:00 PDT" — it shifts into the previous day. The CalDAV time-range filter also returns wrong results for all-day events when UTC arithmetic is applied.

Why it happens: Most date libraries default to UTC datetime handling. The distinction between DTSTART;VALUE=DATE:20260605 and DTSTART;TZID=America/Toronto:20260605T090000 is easy to conflate. Home Assistant's CalDAV integration has had this exact bug filed multiple times.

How to avoid: Represent all-day events as a { date: "YYYY-MM-DD", allDay: true } struct throughout the stack — never coerce to a JS Date or SQL DATETIME. In MariaDB, store as DATE column, not DATETIME. In the API response, emit the ISO date string without a time component. In the frontend, detect allDay and render accordingly without any timezone conversion.

Warning signs:

  • All-day events appearing on the day before in certain timezones
  • CalDAV time-range queries missing all-day events that should be in range

Phase to address: Calendar data model phase. Define the allDay field in the internal schema before writing any persistence or API code.


Pitfall 4: ETag / Sync-Token Incremental Sync Done Wrong

What goes wrong: Two common failure modes. First: the app does a full PROPFIND on every poll instead of using WebDAV-Sync (RFC 6578), burning bandwidth and causing delays at scale. Second: the app uses sync-tokens but doesn't handle 403 (token expired/forgotten by server) — some servers, including iCloud, will drop old tokens, and the app must fall back to full resync. Fastmail uses Cyrus IMAP under the hood and can expire tokens.

For write operations: the app updates a VCALENDAR without sending If-Match: <etag>, so if the event was concurrently modified (e.g., from the primary user's native Fastmail app), the server returns a 412 and the app either silently drops the write or throws an unhandled error.

How to avoid:

  • Always check for {DAV}sync-token support via PROPFIND before using it; fall back to getctag polling if absent
  • Persist the last sync-token in the database and use it on subsequent syncs
  • Handle 403 on sync-token by discarding the token and doing a full resync
  • On all PUT/DELETE operations, include If-Match: <etag> header; handle 412 by fetching the current state, presenting a merge/overwrite choice (even if just "last write wins" for v1)

Warning signs:

  • Polling logs show full PROPFIND responses every cycle rather than delta responses
  • Errors after periods of inactivity that clear on app restart
  • Edits from the native Fastmail app not appearing or being overwritten silently

Phase to address: Calendar sync engine phase. The sync-token + ETag strategy must be in the design before writing the poller — retrofitting is painful.


Pitfall 5: Write-Back Modifying a Single Recurring Instance Corrupts the Series

What goes wrong: When a user edits a single occurrence of a recurring event (e.g., moves next Tuesday's meeting to Wednesday), the correct CalDAV write is to store a VEVENT with RECURRENCE-ID as an additional component in the same VCALENDAR resource. A naive implementation either: (a) writes a new standalone event, leaving the original occurrence intact (duplication), or (b) modifies the master RRULE, changing all future occurrences.

Known Nextcloud and ownCloud bugs document exactly this: "CalDAV: Moving single event from recurring events results in duplicated event."

How to avoid: When editing a single instance: fetch the full VCALENDAR, inject a new VEVENT block with RECURRENCE-ID matching the original instance's DTSTART, and PUT the entire modified VCALENDAR back with If-Match. Do not create a new resource. If editing all future instances, set UNTIL or COUNT on the master rule and create a new recurring series starting from the edit point.

For v1, consider restricting to "edit all instances" only and deferring single-instance overrides to v2 — the complexity is disproportionate to a two-person household.

Warning signs:

  • Editing a recurring event produces two events on the calendar
  • Other instances of the series shift after an individual edit
  • Events with RECURRENCE-ID appearing as standalone items

Phase to address: Event edit UI phase. The decision to support or defer single-instance overrides must be made before the edit form is built.


Pitfall 6: DST Transition Shifts Recurring Events by One Hour

What goes wrong: A recurring event created in summer (UTC-4) that spans a DST boundary (clocks fall back to UTC-5) can shift by one hour on every occurrence after the transition if the VTIMEZONE component is malformed or if the server/client disagrees on which TZID to use. This is the single most-reported CalDAV bug across all implementations.

Why it happens: The iCalendar spec requires a VTIMEZONE block describing the DST rules for the TZID in use. Many libraries emit a minimal or incorrect VTIMEZONE. Fastmail's Cyrus server uses its own TZID database; if the client sends a different TZID alias (e.g., Eastern Standard Time vs America/New_York), the server may misinterpret transitions.

How to avoid:

  • Always use IANA timezone IDs (e.g., America/Toronto) — never Windows-style IDs
  • Use a library that generates correct VTIMEZONE blocks from the IANA tz database (e.g., ical.js or node-ical with tz-data)
  • Test recurring events specifically across the spring and fall DST boundary dates before any calendar milestone is considered done

Warning signs:

  • Events correct in summer that shift by exactly one hour in November
  • TZID values in emitted iCalendar containing spaces or Windows timezone names

Phase to address: Calendar write phase. Add a DST-crossing test fixture before the first release.


Pitfall 7: Personal Calendar Sharing to the Broker Token Is Not Automatic

What goes wrong: The architecture assumes one broker API token reads all calendars (shared family + each member's personal). But a Fastmail API token scoped to the primary account cannot read a different member's personal Fastmail calendar unless that calendar has been explicitly shared using Fastmail's CalDAV sharing model (offer + acceptance flow). This is not automatic; it requires setup steps that involve both Fastmail accounts.

Why it happens: Fastmail's calendar ACLs follow the CalDAV sharing standard: the owner must share the calendar, and the recipient must accept. A single-account API token only sees calendars in that account's homeset plus calendars shared to it and accepted.

Additionally, the wife's personal calendar may not be on Fastmail at all if she uses Apple Calendar as her primary (iCloud calendar). In that case the broker can never read it via CalDAV — there is no cross-service token.

How to avoid:

  • Do a proof-of-concept share in the first calendar spike: share the primary user's personal calendar to a test account, accept it, and verify the broker token sees it via PROPFIND of the homeset
  • Document the manual setup steps for each member's personal calendar as part of the deployment runbook
  • For v1, scope the MVP to the shared family calendar only; add personal calendar overlay only after confirming the share/accept flow works
  • If the wife's personal events live in iCloud (not Fastmail), treat her personal calendar as out of scope or accept an ICS subscription URL approach

Warning signs:

  • Broker token PROPFIND returns only the shared family calendar, not personal calendars
  • Empty calendar list after adding a member

Phase to address: Infrastructure/deployment phase and calendar broker spike. This is a prerequisite that blocks "unified view" features.


Pitfall 8: iOS Web Push Requires "Add to Home Screen" — and Apple Provides No Install Prompt

What goes wrong: On Android, Chrome shows an "Install" banner or button (beforeinstallprompt). On iOS, there is no equivalent browser prompt. The user must manually tap the Safari share sheet, scroll to find "Add to Home Screen," and tap it. A non-technical user who doesn't know this exists will never install the PWA, and therefore will never receive any push notifications.

Why it happens: Apple has explicitly not implemented the Web App Manifest install prompt on iOS. The Add to Home Screen path exists but is discoverable only if you know where to look.

How to avoid:

  • Implement an in-app installation guide with annotated screenshots specific to iOS Safari (share icon → "Add to Home Screen") that appears on first visit when display-mode: browser is detected
  • Use navigator.standalone to detect whether the app is installed and conditionally show the banner
  • Do not assume the wife will find this herself — the onboarding flow for iOS must walk her through it explicitly

Warning signs:

  • No push subscriptions registered for iPhone users
  • Wife accessing the app via browser tab URL, not from the home screen

Phase to address: PWA setup / onboarding phase. The install guide is not a nice-to-have — it is load-bearing for the non-technical user UX constraint.


Pitfall 9: iOS Kills Push Subscriptions Silently After 3 Silent Pushes

What goes wrong: Apple/WebKit enforces userVisibleOnly: true strictly. If the service worker receives a push event and fails to display a notification before the event handler terminates — even once accidentally — iOS counts it as a "silent push." After 3 silent pushes, the subscription is permanently revoked without any pushsubscriptionchange event (which iOS doesn't support anyway). The user stops receiving notifications without knowing it, and the server continues sending to a dead endpoint.

Why it happens: The most common mistake is calling showNotification() without wrapping it in event.waitUntil(). Without waitUntil, the service worker runtime terminates before the async notification display completes, making it appear silent to iOS.

Separately: Apple's Intelligent Tracking Prevention (ITP) deletes service worker registrations for sites not visited "recently enough," silently invalidating subscriptions.

How to avoid:

  • Always wrap showNotification() in event.waitUntil() — no exceptions
  • Implement a subscription health-check: on every app open, call pushManager.getSubscription() and compare the endpoint to the stored server-side endpoint; re-subscribe if they differ or if null
  • On the server, handle 410 (Gone) responses from the push service as permanent subscription deletion; remove the subscription record immediately
  • Handle 404 and 401 from the push service as potentially expired; remove and force re-subscription on next app open
  • Log push delivery success/failure server-side so silent failures are detectable

Warning signs:

  • Push success rate drops from the server perspective with no user-visible errors
  • Server has endpoint records but delivers 410/404
  • Wife's iPhone stops getting notifications after a week of inactivity

Phase to address: Push notification implementation phase. The waitUntil pattern and subscription health-check must be in the initial implementation, not added later.


Pitfall 10: Declarative Web Push vs Standard Web Push — Choose the Right Target

What goes wrong: WebKit has introduced "Declarative Web Push" (announced mid-2025), which allows notification display without a service worker by using a standardized JSON payload format. If you build against standard Web Push with a service worker and custom payload parsing, and then Apple's ITP clears the service worker, your notifications stop. Declarative Web Push survives ITP because the browser can display the notification natively without running JS.

If you build notifications that depend on custom payload processing in the service worker (e.g., fetching additional data from the server before showing the notification), Declarative Web Push cannot handle that — you'd need the service worker anyway.

How to avoid: Design notification payloads to be self-contained (all display data in the push payload: title, body, icon, URL to open). This satisfies both Declarative Web Push display requirements and standard Web Push service worker display. Do not design a "fetch-to-display" pattern where the service worker hits the API before showing anything — that pattern breaks on iOS after ITP clears the SW.

Warning signs:

  • Notification payloads that contain only an event ID, requiring a network fetch to render
  • Service worker push handler making API calls before showNotification

Phase to address: Push notification design phase. Define the payload schema before implementing the server-side push sender.


Pitfall 11: EU Digital Markets Act Breaks iOS PWA Entirely for EU Users

What goes wrong: Since iOS 17.4 (March 2024), in EU countries Apple removed standalone PWA mode under the Digital Markets Act. PWAs open as standard Safari tabs. Push notifications do not work. Add to Home Screen produces a bookmark, not an installed PWA. This affects the entire notification strategy for any EU household.

Why it matters here: The project is Canadian (primary user email: me@lucasberger.ca.ca domain, Unraid self-hosted). If the household is in Canada, this does not apply. Document it as a known non-issue for this deployment, but note it if the household ever relocates or if devices are registered in EU Apple IDs.

How to avoid: Confirm Apple ID region for both household members. If non-EU, proceed without mitigation. If EU, the entire push notification strategy must shift to email or in-app alerts only.

Phase to address: Risk assessment before push implementation. One-time check, not ongoing work.


Pitfall 12: Service Worker Caching Serves Stale Calendar Data

What goes wrong: If the service worker uses a Cache-First strategy for API responses, calendar data shown in the PWA may be hours old. A user adds an event from the native Fastmail app, opens FamilySync, and sees yesterday's calendar. For a family coordination tool this destroys trust immediately.

Why it happens: Cache-First is the default recommendation for PWA shell (HTML/CSS/JS assets) but gets applied to API/data routes by mistake, or by using a broad URL pattern in the Workbox config.

How to avoid:

  • Use Cache-First only for static assets (JS bundles, CSS, icons) with content-hash filenames
  • Use Network-First for all /api/* routes; fall back to cache only if offline
  • Use Stale-While-Revalidate for calendar data that is acceptable to be slightly stale (list data OK; calendar event times not OK)
  • Scope workbox-recipes patterns explicitly; never use .* to match API routes

Warning signs:

  • Network tab shows calendar API responses served from ServiceWorker cache
  • Events created elsewhere don't appear after page reload

Phase to address: PWA service worker configuration phase. Cache strategy per route must be intentional from the start.


Pitfall 13: Service Worker Update Staleness — App Never Updates for the Wife

What goes wrong: Safari on iOS respects the HTTP cache for service worker script fetching. If the server sends Cache-Control: max-age=3600 for the SW script, Safari will not check for updates for an hour. For an installed PWA that the wife only opens occasionally, she can run a version that is days old. Breaking API changes in the backend will cause silent failures.

Why it happens: Many web servers (Nginx defaults) cache JS assets aggressively. The service worker file (sw.js) must be served with Cache-Control: no-cache or max-age=0 specifically to ensure the browser checks for updates on each visit.

How to avoid:

  • Serve the service worker file with Cache-Control: no-cache header explicitly
  • Serve the web app manifest with Cache-Control: no-cache
  • Use Vite's default hashed filenames for all other assets (already correct)
  • Implement a "new version available" in-app prompt when the SW detects an update (waiting state), so the wife knows to tap "refresh"

Warning signs:

  • Deployed backend changes not reflected in the app for hours or days
  • Console shows ServiceWorker: new service worker found, not yet activated

Phase to address: PWA build configuration phase. Set the no-cache header in Docker/Nginx config before first deployment.


Pitfall 14: Calendar Cache Stale vs Fastmail Source of Truth — Double-Write Window

What goes wrong: The app caches Fastmail calendar data in MariaDB (or Redis) for performance. A user creates an event via the app (write to Fastmail, update local cache). The primary user simultaneously creates the same-time event from the native Fastmail app. The next poll catches the conflict, but between the write and the poll, the app's cache is wrong. If the poll interval is 5 minutes, the family sees conflicting events for up to 5 minutes.

A worse failure: the write to Fastmail succeeds but the cache update fails (network error mid-transaction). Now the cache is permanently wrong until the next full resync.

How to avoid:

  • Treat the cache as write-through: invalidate the relevant calendar's cache entry immediately on any write, forcing the next read to pull from Fastmail
  • After a write (PUT/POST to Fastmail), always re-fetch the created/updated object to get the server-assigned ETag and any server-side modifications
  • Never update the cache with the client's version of the object — only cache objects received from the server
  • For v1, a 60-second poll interval is acceptable. Do not optimize this prematurely.

Warning signs:

  • Event created in the app doesn't appear on the next refresh
  • Duplicate events visible for a short window
  • ETag mismatch errors on the second consecutive edit of the same event

Phase to address: Calendar broker / cache design phase. Write-through invalidation must be in the cache design, not patched in later.


Pitfall 15: Real-Time List Sync — Missed Updates During Reconnect Gap

What goes wrong: The client connects via WebSocket (or SSE). The connection drops (mobile network switch, brief outage). On reconnect, the client re-subscribes but has missed events that fired during the gap. The list appears consistent to both users but is actually diverged — one user's added item is missing from the other's view.

Why it happens: The reconnect handler re-subscribes from "now" rather than replaying from a sequence number or version cursor.

How to avoid:

  • Each list mutation must increment a monotonic version on the row (updated_at with microsecond precision is insufficient — use an explicit integer sequence per list)
  • On (re)connect, the client sends its last-known sequence; the server replays any mutations with sequence > client's last-known
  • Implement exponential backoff with jitter on reconnect (500ms base, 2x multiplier, 30s cap)
  • Redis Pub/Sub is appropriate here; if Redis is unavailable, fall back to polling every 5s

Warning signs:

  • Items added during a reconnect gap missing from one client's view
  • List state inconsistent between the two household members

Phase to address: Shared lists implementation phase. The sequence number column must be in the initial schema.


Pitfall 16: Authelia OIDC — v4.39+ Breaking Change Drops groups from ID Token

What goes wrong: Authelia v4.39 introduced a breaking change: the groups claim is no longer included in the ID token by default — it moved to the userinfo endpoint. If the backend validates authorization based on the groups claim in the ID token (a common pattern when following older Authelia docs), group-based access control silently stops working after an Authelia upgrade.

How to avoid:

  • Request the groups scope explicitly in the OIDC client config
  • Validate group membership by calling the userinfo endpoint, not by reading ID token claims
  • Or: use Authelia purely for authentication (who are you), not authorization (what can you do) — in a two-person household, all authenticated users are trusted, so groups are irrelevant for FamilySync

Warning signs:

  • Group-based middleware that worked before an Authelia upgrade stops denying unauthorized users
  • groups claim missing from decoded ID token

Phase to address: Auth integration phase. Decide whether group claims are needed at all; if not, skip them entirely.


Pitfall 17: Authelia OIDC — SPA Token Silent Renewal Breaks When Authelia Is on a Separate Domain

What goes wrong: The standard OIDC silent renewal technique for SPAs uses a hidden iframe that loads Authelia's authorization endpoint. If the browser blocks third-party cookies (Safari ITP does this aggressively), the iframe cannot send Authelia's session cookie, so the silent renewal returns an error and the user is redirected to the login page unexpectedly — often in a loop.

Why it matters here: FamilySync and Authelia may be on different subdomains (e.g., familysync.home.domain.com vs auth.home.domain.com). If they share the same registrable domain suffix (e.g., both .home.domain.com), same-site cookies work. If not, ITP kills the iframe flow.

How to avoid:

  • Ensure FamilySync and Authelia share the same parent domain so Authelia cookies are same-site from the browser's perspective
  • Use refresh token rotation instead of iframe-based silent renewal (Authelia supports this)
  • Configure the OIDC client in the backend (not the SPA) to hold the refresh token — the SPA calls the backend, which silently renews via the confidential client flow, and returns a new access token without iframe involvement
  • Never rely on iframe silent renewal for installed PWAs — service workers intercept the iframe navigation and it behaves unpredictably

Warning signs:

  • Users randomly logged out after token expiry with no warning
  • Infinite redirect loop between the app and Authelia login page
  • Console errors: Failed to load resource: Frame load interrupted

Phase to address: Auth integration phase. The token refresh strategy must be decided before the frontend auth client is chosen.


Pitfall 18: Pangolin/Newt Tunnel — WebSocket and SSE May Require Explicit Configuration

What goes wrong: Pangolin is a tunneled reverse proxy. WebSocket connections require an HTTP Upgrade handshake, and SSE (Server-Sent Events) requires long-lived HTTP connections. A generic proxy configuration that works for standard HTTP requests may silently drop WebSocket connections or close SSE streams after a timeout.

A known GitHub issue (#1034 on the fosrl/pangolin repo) documents exactly this: HTTP loads fine through Pangolin but WebSocket connections to wss:// fail.

How to avoid:

  • Verify WebSocket pass-through in a dedicated infrastructure spike before building any real-time feature
  • Confirm Pangolin's timeout settings for long-lived connections and extend them appropriately
  • If WebSocket through Pangolin proves unreliable, use SSE (unidirectional, standard HTTP, more proxy-friendly) for server-to-client push and short-poll for client confirmations
  • Test the full round-trip (WebSocket from an iPhone over Pangolin) before considering real-time sync "done"

Warning signs:

  • WebSocket connects locally but fails in production (public URL)
  • SSE stream closes after 60 seconds with no activity
  • Real-time updates work on Android (same network) but not iPhone (over tunnel)

Phase to address: Infrastructure spike phase, before real-time list sync is implemented.


Technical Debt Patterns

Shortcut Immediate Benefit Long-term Cost When Acceptable
Full PROPFIND poll on every sync interval instead of WebDAV-Sync Simpler code Bandwidth waste; shows up immediately at any real-world poll frequency Never — implement sync-token from day one
Client-side RRULE expansion instead of server CALDAV:expand Avoids REPORT query complexity DST and override bugs that are very hard to diagnose Never for authoritative display; OK for UI-only preview
Storing all-day events as DATETIME in MariaDB Avoids DATE type handling Timezone shift bugs on display Never
Skip If-Match ETag on write-back Simpler write path Silent data loss on concurrent edits Acceptable for v1 only if single-writer constraint is documented; still risky
iframe silent token renewal instead of refresh token rotation Less backend work Breaks on iOS Safari ITP; causes random logouts Never for this stack
Cache-First strategy for API responses Fast perceived performance Stale calendar/list data shown as current Never for data routes
Notification payload requiring server fetch before display Richer notifications Violates userVisibleOnly; kills iOS subscription after 3 events Never
Single-instance recurring event override deferred Significantly simpler write logic Users frustrated when editing "just this one" is impossible Acceptable for v1 — document clearly

Integration Gotchas

Integration Common Mistake Correct Approach
Fastmail CalDAV Using bare caldav.fastmail.com as base URL Use https://caldav.fastmail.com/dav/principals/user/{email}/
Fastmail CalDAV Assuming JMAP supports calendars CalDAV only; JMAP calendar spec not yet production at Fastmail
Fastmail CalDAV Assuming broker token sees all members' personal calendars Explicit share+accept required per calendar per account
CalDAV ETag Assuming ETag is always returned after PUT Sometimes absent; always re-fetch after write
CalDAV sync-token Not handling 403 on expired token Fall back to full PROPFIND resync when token is rejected
CalDAV recurring events Writing RECURRENCE-ID override as a new resource Must be an additional VEVENT in the same VCALENDAR resource
iOS Web Push Calling requestPermission() outside a click handler Permission silently denied; must be in direct user gesture handler
iOS Web Push Not wrapping showNotification() in event.waitUntil() 3 silent pushes = permanent subscription revocation
iOS Web Push Not handling 410 Gone from push service Dead subscriptions accumulate; subscription table grows without cleanup
Authelia OIDC v4.39+ Reading groups from ID token Fetch from userinfo endpoint or skip groups entirely
Authelia OIDC iframe silent renewal with ITP Use refresh token rotation via confidential backend client
Pangolin tunnel Assuming HTTP proxy config handles WebSocket Requires explicit WS upgrade passthrough and timeout config

Performance Traps

Trap Symptoms Prevention When It Breaks
Full calendar PROPFIND on every poll High Fastmail API usage; slow sync Use WebDAV-Sync (sync-token) for delta fetches From first deployment
Expanding RRULE in-process for a 2-year window CPU spike on backend; slow calendar load Limit expansion to visible time window via CALDAV:expand Any calendar with >30 recurring events
Fetching all VCALENDAR objects to find changed ones N+1 CalDAV requests per sync Use PROPFIND to get ETags first; only GET objects with changed ETags Immediately on any non-trivial calendar
WebSocket broadcast to all clients on every list mutation Unnecessary events to clients not viewing that list Filter broadcasts by list membership/subscription Once any second device is connected

Security Mistakes

Mistake Risk Prevention
Storing Fastmail API token in client-accessible storage Token leak → full Fastmail account calendar access Token lives only in backend env vars; never exposed to frontend
Forwarding raw VCALENDAR to the frontend iCalendar payloads can contain injected properties Parse and reconstruct a safe JSON event object server-side
Not validating OIDC aud claim Any Authelia client can impersonate FamilySync users Verify aud matches your registered client ID on every token validation
Push subscription endpoint stored unencrypted with PII Subscription URLs are tied to device identity Store encrypted; treat as sensitive PII; delete on logout
Trusting display-mode: standalone for access control Cannot be relied on for security Standalone check is UX-only; all access control at API layer via token

UX Pitfalls

Pitfall User Impact Better Approach
No iOS install guide in the app Wife never installs PWA; never gets push notifications Show install CTA with Safari-specific screenshots on first browser visit
Push permission requested on first page load iOS silently denies; permission cannot be re-requested Request permission only after explaining why, inside a tapped button
"Notification" as the only feedback for list changes Missed if phone silent; no in-app indicator Show badge/dot on list items changed since last viewed; push is secondary
Calendar event edit with no conflict warning Overwrites event changed in native Fastmail app Show "this event was modified elsewhere" warning on 412; offer overwrite or reload
No offline message App appears broken when offline Show "offline — viewing cached data" banner; list edits queue for sync
Recurring event edit options not explained User edits "this event" not knowing it changes all future events For v1, edit-all only with clear label "This changes all future events"

"Looks Done But Isn't" Checklist

  • Calendar sync: Often missing ETag/sync-token handling — verify the poller uses delta sync, not full PROPFIND, on the second and subsequent runs
  • All-day events: Often missing allDay flag — verify a June 5 all-day event appears on June 5 in UTC-5 timezone, not June 4
  • Recurring events: Often missing DST boundary test — verify a weekly recurring event set in July still shows at the correct hour in November
  • iOS push: Often missing subscription health-check — verify a fresh subscription is created after ITP clears the service worker (simulate by clearing site data in Safari settings)
  • iOS push: Often missing event.waitUntil() — verify in Safari DevTools that push events show as resolved, not terminated
  • Personal calendar sharing: Often assumed to be automatic — verify broker token PROPFIND actually returns the wife's personal calendar URL
  • Write-back: Often missing 412 handling — verify the app does not silently drop an edit when the event was modified concurrently in the native app
  • Service worker update: Often ships with default asset caching headers — verify sw.js is served with Cache-Control: no-cache
  • Auth token renewal: Often untested at expiry — verify the app does not redirect to login when the access token expires mid-session
  • Pangolin WebSocket: Often only tested on LAN — verify WebSocket or SSE works end-to-end over the Pangolin public URL before shipping real-time sync

Recovery Strategies

Pitfall Recovery Cost Recovery Steps
JMAP calendar assumption baked in HIGH Rewrite broker protocol layer; API contract may change
All-day event stored as DATETIME HIGH Migration required; all calendar cache invalid; must resync
No ETag/sync-token on poller MEDIUM Swap polling logic; no data loss, but adds a sprint
Push subscription table full of dead iOS endpoints LOW Run cleanup job: send to each endpoint, delete 410/404 responses
Personal calendars not shared to broker LOW Manual Fastmail share + accept; no code change
VTIMEZONE DST bug in emitted iCalendar MEDIUM Swap tz library; clear Fastmail events and re-sync
Service worker cache serving stale data LOW Add route-specific Workbox config; redeploy
Authelia groups claim removed after upgrade LOW Update claim source to userinfo endpoint; or remove group checks
Pangolin WebSocket drops MEDIUM Switch real-time transport from WS to SSE; no data model change

Pitfall-to-Phase Mapping

Pitfall Prevention Phase Verification
JMAP calendar not available Calendar broker spike (Phase 1) Confirm CalDAV endpoint returns events before any further work
RRULE client-side expansion Calendar fetch design (Phase 1) REPORT with CALDAV:expand in the first integration test
All-day event DATE vs DATETIME Data model design (Phase 1) Schema review; allDay field present; timezone test
ETag / sync-token incremental sync Calendar sync engine (Phase 1-2) Second poll must use sync-token; verify in network logs
Write-back RECURRENCE-ID corruption Event edit UI (Phase 2-3) Integration test: edit instance, verify series unchanged
DST timezone shift Calendar write phase (Phase 2) Dedicated DST fixture test before milestone sign-off
Personal calendar sharing Infrastructure/deployment (Phase 1) Manual proof-of-concept share before unified view is built
iOS install guide missing PWA onboarding (Phase 2) Test with non-technical user; do not consider done until wife installs
iOS push subscription killed (silent push) Push implementation (Phase 2-3) Verify event.waitUntil() pattern; test 3 consecutive pushes
Declarative Web Push payload design Push design (Phase 2) Payload schema review before server-side push sender is built
EU DMA restriction Risk assessment (Phase 1) One-time check of Apple ID regions; document conclusion
Service worker stale cache (data routes) PWA SW config (Phase 2) Network tab audit: API calls must not be served from cache
SW update staleness PWA build config (Phase 2) Verify sw.js has Cache-Control: no-cache in Nginx config
Calendar cache double-write Cache design (Phase 1-2) Write-through invalidation test: write → immediate re-fetch from Fastmail
List sync reconnect gap Lists implementation (Phase 2) Disconnect mid-edit test; verify item appears after reconnect
Authelia groups claim breaking change Auth integration (Phase 1) Decode ID token; confirm no groups dependency; document decision
Authelia SPA silent renewal Auth integration (Phase 1) Test token expiry behavior before frontend is considered done
Pangolin WebSocket passthrough Infrastructure spike (Phase 1) WebSocket smoke test over public URL before real-time feature is built

Sources


Pitfalls research for: Fastmail-brokered family calendar + shared-list PWA with Web Push, self-hosted behind Authelia Researched: 2026-06-03