Files
familysync/.planning/research/FEATURES.md
T
Lucas BergerandClaude Opus 4.8 6e1c9ca924 docs: complete v1.2 research (stack, features, architecture, pitfalls)
- STACK.md: google-auth-library@10.7.0 + @googleapis/calendar@15.0.0 scoped packages (vs monolithic googleapis), OAuth2 flow, token storage, Google Calendar API event/reminder model
- FEATURES.md: 6 feature categories (multi-provider, self-service onboarding, multiple reminders, dark mode, zero-setup DB, dev/CI stub), dependency graph, feature prioritization
- ARCHITECTURE.md: CalendarProvider interface, provider factory, CalDavProvider wrapper, GoogleCalendarProvider, MockProvider, provider_tokens table schema, multi-reminder JSON column, OAuth callback routing, 7-component data flows
- PITFALLS.md: 11 critical/medium pitfalls (refresh token 7-day expiry in testing status, Google recurrence mismatch, syncToken 410, timezone handling, provider abstraction regression, VALARM dedup key, auto-migrate failures, dark mode FOWT, OAuth callback through tunnel, token encryption, ESLint 10 breaking changes)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 09:27:35 -04:00

33 KiB
Raw Blame History

Feature Research

Domain: Family calendar PWA — v1.2 new capabilities (multi-provider, theming, zero-setup DB, dev/CI stub) Researched: 2026-06-19 Confidence: MEDIUM (Google Calendar API: MEDIUM via Context7/official docs; UX patterns: LOW via websearch, cross-checked across multiple sources and grounded in the existing codebase)


Scope of This Document

This document covers only the six v1.2 feature areas. v1.0/v1.1 features (calendar display, event CRUD, lists, push, OIDC, local auth, admin settings, setup wizard, per-event reminders, CI harness) are shipped and validated; they appear only as dependencies here.


Category 1: Multi-Provider Calendar Support

What "Good" Looks Like

Apps like Skylight, Cozi, Morgen, and Fantastical converge on the same model: each provider/calendar account gets a unique color lane; events are visually identified by their source calendar's color; event detail view names the source calendar. Provider selection on event creation is a calendar/account dropdown that defaults to a configured "default calendar" — the user picks which calendar to write to before saving.

There is no industry-standard "provider badge icon" per event in the calendar grid. Color is the primary (and sufficient) differentiator; the calendar name in the event detail view is the secondary indicator.

Table Stakes

Feature Why Expected Complexity Notes
Google Calendar events visible in the unified view alongside Fastmail events The core value of multi-provider is the unified display; without it there is no v1.2 HIGH Requires provider abstraction (prerequisite) + Google Calendar API integration + self-service onboarding
Per-calendar color assignment covering Google calendars Users expect events to be visually distinct by source; Skylight and Cozi both do this LOW Phase 17 color-routing already exists for Fastmail; extend the same mechanism to cover Google calendar IDs
Calendar picker on event creation — select which calendar/provider to write to Without this, writes go to an ambiguous default; users lose trust in where events land MEDIUM Lists all writable calendars across all connected providers; defaults to the member's "default calendar" setting; requires the provider abstraction to expose a unified calendar list
Calendar name shown in event detail view Users need to know where the event lives, especially when both Fastmail and Google events are visible LOW Extend the existing event detail sheet with a "Calendar: [name]" line; already implicit in the data model

Differentiators

Feature Value Proposition Complexity Notes
Cross-provider event create — write to Fastmail or Google per event Household members write to whichever calendar they actually use natively MEDIUM Write path must route to the correct provider's outbox; clean once the provider abstraction exists
Per-provider sync status indicator visible in the calendar view header or Settings Non-technical users immediately see if a provider is disconnected without hunting through menus LOW Badge or dot on provider icon; links to Settings for reconnect action

Anti-Features

Feature Why Requested Why Problematic Alternative
Automatic color re-assignment when a new provider is added Seems helpful to "harmonize" colors Scrambles existing user expectations — the wife has learned which color means which calendar Keep existing Fastmail calendar colors; assign new Google calendar colors from unused palette slots
Two-way sync between Fastmail and Google (event mirroring) "I want the same event on both calendars" Infinite loop risk; duplicate events; out of milestone scope Each event lives in exactly one provider; the unified view makes this invisible to the user
CalDAV subscription URL for Google Calendar Some power users subscribe to Google Calendar via CalDAV Google deprecated unauthenticated CalDAV access; fragile and undocumented workaround Use the Google Calendar REST API directly (already planned)

Complexity and Dependencies

  • Keystone dependency: The CalendarProvider interface (provider abstraction) must be defined and Fastmail refactored behind it before Google can be added as a second implementation. This must be Phase 21.
  • Depends on: Self-service onboarding (Category 2) — Google events cannot appear until the member has connected their Google account.
  • Existing feature dependency: the Fastmail broker in apps/api/src/broker/ must be refactored behind a CalendarProvider interface; this is the highest-complexity step in v1.2.

Category 2: Self-Service Provider Onboarding

What "Good" Looks Like

The industry-standard pattern (GoHighLevel, Zapier, Morgen, Fantastical) distinguishes two onboarding flows:

  • App-password providers (Fastmail): Short form — credential input + optional endpoint URL + "Verify & Save" button. Live validation: the app attempts a CalDAV PROPFIND before persisting. Error state names the problem in plain language ("Wrong password", "Can't reach Fastmail").
  • OAuth providers (Google): Single "Connect Google Calendar" button → browser redirect to Google's consent screen → user approves → redirect back with auth code → app exchanges for access + refresh tokens → stores encrypted refresh token in DB. The token is never shown to the user.
  • Status indicators: After connection, each provider shows a health badge (green = healthy, yellow = expiring soon, red = disconnected). The red state shows a prominent "Reconnect" button — one tap re-runs the OAuth flow without removing and re-adding the connection.

The reconnect pattern is non-negotiable for the non-technical Apple member. When her Google token expires (password change, 6-month inactivity, suspicious activity), she must see a visible banner in the calendar view — not discover the problem by noticing missing events days later.

Table Stakes

Feature Why Expected Complexity Notes
"Connect Google Calendar" OAuth button in member Settings OAuth is the expected UX for Google — no one expects to paste an API key MEDIUM googleapis generateAuthUrl({access_type:'offline', scope:['https://www.googleapis.com/auth/calendar']}) + callback route + store encrypted refresh token in member_credentials
One-tap "Reconnect" button when Google token expires or is revoked Token churn is inevitable; must not require removing and re-adding the connection MEDIUM Re-runs the OAuth flow; existing member_credentials row updated in place
Fastmail app-password form with live CalDAV validation (member self-service) Fastmail credential entry already exists in admin (Phase 10); now surfaced to the member in their own Settings LOW Reuse/move the existing CredentialSheet component and /api/me/credential route from Phase 10; already validated
Connection health status per provider shown in member Settings Non-technical users need ambient awareness that their calendar is connected LOW GET /api/me/providers returns per-provider status; show green/red indicator per entry
Error states that name the problem without technical jargon "Your Google Calendar isn't connected" not "401 Unauthorized" LOW UX copy layer over API error codes; inline on the Settings page
Proactive disconnect banner on the calendar view itself User sees the issue before discovering missing events LOW Banner above the calendar when any provider is in error state; links to Settings reconnect

Differentiators

Feature Value Proposition Complexity Notes
"Test connection" re-check button after credential save Confirms the connection still works without navigating away LOW Same live-validation call as initial save; reusable endpoint

Anti-Features

Feature Why Requested Why Problematic Alternative
Service-account / server-side Google auth (one token for the whole app) Seems simpler Service account can only access calendars it owns; per-user OAuth is required to write to a member's own Google Calendar Per-user OAuth with per-member refresh token stored in member_credentials
Storing Google OAuth tokens in a browser cookie or localStorage Convenient for the frontend Security anti-pattern — long-lived refresh tokens can be exfiltrated from the client Encrypt and store in member_credentials table server-side, same as Fastmail app passwords
JMAP for Fastmail calendar connection Seems natural given Fastmail's JMAP investment JMAP Calendar API is not available on Fastmail as of 2026 CalDAV via tsdav (locked decision, documented in CLAUDE.md)

Google OAuth Technical Notes (MEDIUM confidence — official docs via Context7)

  • Scope: https://www.googleapis.com/auth/calendar — read + write events + calendar list. Classified as a "restricted" scope but no Google verification is required for self-hosted personal apps with fewer than 100 users (all known to the operator).
  • access_type: 'offline' is required to receive a refresh token on the first authorization.
  • Refresh tokens are only issued on first authorization unless prompt: 'consent' is forced. Listen for the tokens event and persist immediately.
  • Token auto-refresh: setting oauth2Client.setCredentials({refresh_token}) causes googleapis to auto-refresh the access token transparently — no manual refresh loop needed in normal operation.
  • Revocation handling: catch err.status === 401 from refreshAccessToken(), clear the stored token from DB, surface the reconnect banner.
  • Google's refresh token revocation triggers: user changes Google password, user revokes app access in Google account settings, Google detects suspicious activity, token unused for 6 months.

Complexity and Dependencies

  • Depends on: Provider abstraction (Category 1) — credentials are stored and managed per-provider through the abstraction.
  • Existing feature dependency: member_credentials table exists (Phase 10); needs provider_type = 'google' and an encrypted refresh_token column.
  • Existing feature dependency: admin credential rotation patterns from Phase 10 are directly reusable for self-service credential management.
  • OAuth callback route must work inside the Safari standalone PWA on iOS — same constraint as the Authelia OIDC redirect in Phase 3 (load-bearing; must be validated on device).

Category 3: Multiple Reminders Per Event

What "Good" Looks Like

Google Calendar: unlimited reminders via an "Add notification" link below the reminder list; each row is a lead-time picker + remove button. Apple Calendar: hard cap of 2 alerts ("Alert" + "Second Alert" fields — no add button beyond 2). Outlook: up to 2. Morgen: follows Google's unlimited model.

For a family PWA, cap at 5 reminders. Real-world usage clusters at 12 per event ("1 day before" + "30 min before" is the canonical pair). The UI pattern is a vertical list of reminder rows, each with the existing lead-time picker and an X (remove) button; an "Add reminder" link below the list is disabled once the cap is reached.

Table Stakes

Feature Why Expected Complexity Notes
Add up to 5 reminders per event (list UI with add/remove per row) Google Calendar sets this expectation; "1 day before AND 30 min before" is the most common household pattern MEDIUM The existing reminderLeadMinutes single-integer field (Phase 11) must be refactored to an array. VALARM serialization already exists (buildTimedValarm/buildAllDayValarm) — extend to emit N VALARMs
Remove any individual reminder from the list Without remove, users accumulate unwanted reminders over time LOW X button on each reminder row; list collapses to empty = no reminder (same as "None" today)
Pre-populate all existing reminders on event edit Phase 11 pre-populates one reminder; must now pre-populate all N LOW classifyValarms/extractValarms (Phase 11) already return arrays — thread the full array to the form
Sensible cap with clear feedback Prevents absurd configurations (10+ reminders per event) LOW Cap at 5; "Add reminder" link grays out at cap; no error toast needed
Google Calendar provider: translate reminders to event.reminders.overrides format Google Calendar uses {method:'popup', minutes:N} objects, not VALARM MEDIUM The provider abstraction's write path translates the internal reminder array to the correct format per provider; Fastmail → VALARMs, Google → reminders.overrides

Differentiators

None — multiple reminders is table stakes from Google Calendar's baseline. Doing it without reminder duplication on edit and without stripping non-FamilySync VALARMs is what good execution looks like.

Anti-Features

Feature Why Requested Why Problematic Alternative
Reminder templates ("always remind me 1 day before for all my events") Seems like a time-saver Premature complexity; per-event reminder list already covers the use case Per-event reminder list; per-user default reminder is a v1.3 consideration
Reminder snooze from the notification payload RFC 9074 defines this; Fantastical supports it Requires a write back to the calendar from the service worker notification handler — a high-complexity, high-fragility path for a rare use case Dismiss; re-add a reminder manually if needed

Complexity and Dependencies

  • Depends on: existing reminderLeadMinutes single-value field (Phase 11) — must migrate to a JSON array column (additive migration; old column deprecated, not dropped, to avoid downtime).
  • Existing feature dependency: buildTimedValarm/buildAllDayValarm/classifyValarms/extractValarms/computeAlertInstantUtc in the broker (Phase 11) — all handle arrays natively (ical.js returns arrays); only the serialization call site changes.
  • Existing feature dependency: variable-lead scheduler (Phase 11) — must schedule N reminders per event; dedup key changes from uid:dtstartMs to uid:dtstartMs:leadMinutes.
  • Ordering note: this can be implemented in parallel with provider abstraction, but the Google provider translation layer (reminders.overrides) is naturally built alongside the Google provider (Category 1/2).

Category 4: PWA Dark Mode / Theming

What "Good" Looks Like

Three options: System (follows OS prefers-color-scheme), Light, Dark. The toggle lives in user Settings — not admin-only, both members control their own theme independently. Preference is persisted in localStorage (no server round-trip; no DB column needed). No flash-of-wrong-theme (FOIT) on load.

Phase 17 already shipped semantic CSS custom properties (tokens.css) on :root — the groundwork is done. Dark mode requires: a [data-theme="dark"] override block in tokens.css, a blocking inline script in index.html <head> to set the attribute before first paint, and a three-option toggle in Settings.

Table Stakes

Feature Why Expected Complexity Notes
Respect OS prefers-color-scheme by default Every modern app does this; iOS Dark Mode is heavily used by Apple users LOW CSS-only: @media (prefers-color-scheme: dark) override for [data-theme="system"] tokens; no JS needed for system mode
Explicit System / Light / Dark toggle in Settings Users override OS preference for specific apps routinely LOW Three-option segmented control in the Settings page (not admin-gated)
No flash-of-wrong-theme on PWA load FOIT is jarring and immediately noticeable — the non-technical Apple member will notice MEDIUM Blocking inline <script> in index.html <head> reads localStorage and sets data-theme synchronously before Vite's JS bundle parses; must be a raw inline script (not type="module")
Preference persisted in localStorage Theme must survive PWA re-launch and page refresh LOW `localStorage.setItem('theme', 'dark'
Calendar event colors readable in dark theme Color-coded lanes are the core UX — they must not become unreadable on dark backgrounds MEDIUM Ensure hue-based event colors (rose, blue, amber) have sufficient contrast on the dark surface token; may need adjusted lightness values in the dark token override block

Differentiators

None — dark mode is table stakes in 2026 for any consumer app. The differentiator is doing it without FOIT.

Anti-Features

Feature Why Requested Why Problematic Alternative
Per-member theme synced to the server (follows you across devices) "My theme should be consistent on any device" Adds a DB column + API round-trip for a cosmetic preference; minimal benefit for a 2-person household localStorage only; set the preference on each device separately
Custom color theme editor Power user appeal Scope creep; the semantic token system supports it eventually but not in v1.2 System/light/dark is the complete feature for this milestone

Complexity and Dependencies

  • Depends on: Phase 17 semantic token layer (tokens.css) — already shipped; this feature extends it with a dark override block.
  • Low implementation risk — primarily CSS changes plus a small inline script in index.html.
  • Vite note: the blocking inline script must be injected into index.html as a literal string (use Vite's HTML transform plugin or a direct <script> tag in index.html). It cannot be an ES module (type="module") because modules are deferred and execute after paint.
  • Independent of all other v1.2 features — can be developed in a parallel phase with no ordering constraint.

Category 5: Zero-Manual-Setup DB Bootstrap

What "Good" Looks Like

The operator provides a MariaDB host, port, DB name, username, and password in env vars. On docker compose up, the API boots, calls Drizzle's migrate() programmatically before the HTTP server starts accepting requests, and all tables are created. On subsequent boots, migrate() is idempotent — already-applied migrations are skipped via the __drizzle_migrations tracking table. The operator never runs drizzle-kit migrate manually.

Table Stakes

Feature Why Expected Complexity Notes
Programmatic migrate() call in API boot sequence, before serve() starts Self-hosted operators expect "docker compose up and it works" — hand-running CLI migrations is a deployment barrier LOW import { migrate } from 'drizzle-orm/mysql2/migrator'; call in apps/api/src/index.ts before the Hono server binds; wrap in try/catch with process.exit(1) on failure
Idempotent on repeated boot Containers restart; migrations must not fail on the second run LOW Built into drizzle-kit migrate: tracks applied migrations in __drizzle_migrations; already-applied migrations are no-ops
DB readiness wait before migrating The MariaDB container may not be ready when the API container starts LOW Retry loop (5 attempts, 2s exponential backoff) using SELECT 1 before calling migrate(); process.exit(1) after all retries fail with a clear log message
Migration failure = fatal startup with a clear log message Silent failure leaves the app running against a broken schema LOW console.error('DB migration failed:', err) + process.exit(1)

Differentiators

None — zero-setup bootstrap is the baseline operator expectation for modern self-hosted apps. Meeting it cleanly is the goal; there is no competitive angle here.

Anti-Features

Feature Why Requested Why Problematic Alternative
drizzle-kit push in production Simpler — no SQL migration files to manage Destructive on schema conflicts; drops columns without warning; not safe for a production DB with real user data drizzle-kit generate (committed SQL files) + programmatic migrate() at boot
App creates the database itself (not just tables) Operators shouldn't need to pre-create the DB Requires elevated CREATE DATABASE privileges that operators may not want to grant the app user Document in deployment guide: "create a database named familysync and grant your app user full access to it" — 30 seconds of manual setup that avoids privilege escalation

Complexity and Dependencies

  • Depends on: existing Drizzle migration files in apps/api/src/db/migrations/ (all phases — already exist).
  • Existing constraint from Phase 8/16 (D-20): MariaDB 11 does not have mysqladmin ping; the in-app readiness check must use a raw SELECT 1 query via mysql2, not any CLI command.
  • Low implementation risk: drizzle-orm/mysql2/migrator is already a transitive dependency; only the boot sequence wiring is new code.
  • Enables: the existing setup wizard (Phase 12) to assume the schema exists when it runs — clean separation of concerns (schema bootstrap → setup wizard → admin config).

Category 6: Dev-User Full-App Exercise (Stub Calendar Provider)

What "Good" Looks Like

A StubCalendarProvider that implements the same CalendarProvider interface as Fastmail and Google, returning deterministic seed events from memory and mutating an in-memory Map on writes. Activated by CALENDAR_PROVIDER=stub (implicitly active alongside DEV_AUTH_BYPASS=true). The Playwright CI harness uses the stub so tests are hermetic — no live CalDAV or Google API calls, no Fastmail credentials required in CI.

Seed events must cover: timed events, all-day events, a recurring weekly series, events with reminders, past events, and future events — enough to exercise the full UI, reminder pipeline, and RRULE expansion without gaps.

Table Stakes

Feature Why Expected Complexity Notes
CalendarProvider interface with fetchEvents, createEvent, updateEvent, deleteEvent Required before any stub can exist; is also the keystone for provider abstraction (Category 1) MEDIUM Define in apps/api/src/broker/providers/CalendarProvider.ts; Fastmail and Google are implementations; stub is the third
Stub provider with seeded deterministic events Developers and CI must exercise full event CRUD + reminder + multi-provider display without a live calendar MEDIUM In-memory Map<string, CalendarEvent>; seed data covers event varieties needed by Playwright tests; writes mutate the map and are visible to subsequent fetches in the same process
Stub activated by env var (CALENDAR_PROVIDER=stub) Must be opt-in; never active in production LOW Read env at startup in provider resolution; route CalendarProvider selection accordingly
Stub integrated with Playwright CI harness The existing CI harness (DEV_AUTH_BYPASS=true) must also activate the stub so calendar tests pass without Fastmail credentials LOW Add CALENDAR_PROVIDER=stub to the CI workflow env block alongside DEV_AUTH_BYPASS=true; no other change needed
Reminder scheduling works against stub events Exercises the full reminder pipeline in CI without live events LOW Stub seed data includes events with reminder leads set; the scheduler fires against them normally

Differentiators

Feature Value Proposition Complexity Notes
Stub resets on API restart (intentionally stateless) Keeps CI tests hermetic — no state leaks between test runs LOW In-memory Map is wiped on process restart; document this behavior explicitly
Seed events designed to cover Phase 11 edge cases Exercises RRULE expansion, all-day timezone handling, multi-VALARM, and past/future reminder scheduling in CI MEDIUM Design seed data to match the Phase 11 test matrix: one recurring weekly event, one all-day multi-day event, one event with 2 reminders, one past event, one future event with 2-day reminder lead

Anti-Features

Feature Why Requested Why Problematic Alternative
Persisting stub events to SQLite or a test-specific MariaDB table "So writes survive restart in dev" Adds a dependency; defeats the hermetic purpose of a stub; blurs the line between dev and test state Use a real Fastmail or Google provider for persistent personal dev; stub is for CI only
Shared stub state across parallel Playwright workers Enables cross-test assertions Race conditions on concurrent create/delete cause flaky tests Each test must be self-contained; rely on seed data, not prior-test writes

Complexity and Dependencies

  • Depends on: CalendarProvider interface — the stub is the third implementation; the interface must be defined before the stub can exist.
  • Ordering note: the stub is needed early in v1.2 to unblock Playwright CI testing of multi-provider features before live Google credentials are available in CI. It should be implemented in Phase 21 alongside the provider interface.
  • Existing feature dependency: DEV_AUTH_BYPASS pattern (Phases 7/8/13) — piggyback on the same env-var convention; no new CI infrastructure required.

Feature Dependencies

CalendarProvider interface (provider abstraction)
    ├──required by──> Fastmail broker refactor (existing broker → implements interface)
    ├──required by──> Google Calendar provider implementation
    ├──required by──> Stub provider (dev/CI mode)
    └──required by──> Calendar picker on event creation (needs unified provider list)

Stub provider
    └──enables──> hermetic Playwright CI tests for all multi-provider features

Self-Service Google OAuth Onboarding
    └──required by──> Google Calendar events visible in unified view
    └──depends on──> member_credentials table (Phase 10, exists — needs google columns)
    └──depends on──> CalendarProvider interface (credential storage is provider-scoped)

Multiple Reminders per Event
    └──depends on──> reminderLeadMinutes field (Phase 11, exists — must become JSON array)
    └──depends on──> VALARM serialization helpers (Phase 11, exist — handle arrays natively)
    └──depends on──> variable-lead scheduler (Phase 11, exists — dedup key update only)
    └──interacts with──> Google provider (reminders.overrides format translation)

Zero-Setup DB Bootstrap
    └──depends on──> Drizzle migration files (all phases, exist)
    └──enables──> clean first-run: bootstrap schema → setup wizard → admin config

PWA Dark Mode
    └──depends on──> semantic tokens in tokens.css (Phase 17, exists)
    └──independent of all other v1.2 features (fully parallelizable)

Dependency Notes

  • Provider abstraction is the keystone. Google Calendar, the calendar picker, and the stub all require CalendarProvider to be defined and Fastmail refactored behind it. This is Phase 21 and must complete before any other multi-provider phase starts.
  • Stub enables CI for everything else. Once the interface exists and the stub is wired to DEV_AUTH_BYPASS, all subsequent multi-provider phases get hermetic CI for free.
  • Multiple reminders requires a DB migration. reminderLeadMinutes (single integer) becomes a JSON array column. Use an additive migration — add new column, keep old column until v1.3 cleanup — to avoid breaking the running app during deploy.
  • Dark mode and zero-setup DB are independent of all multi-provider work and can be developed in a parallel phase with no ordering constraints.
  • OAuth callback on iOS Safari standalone PWA. The Google OAuth redirect-back must work in Safari standalone mode on the wife's iPhone — the same load-bearing constraint as the Authelia OIDC redirect in Phase 3. Must be a human gate before the Google onboarding feature is marked validated.

v1.2 Feature Prioritization

Feature User Value Implementation Cost Priority
CalendarProvider interface + Fastmail refactor HIGH (unlocks everything multi-provider) MEDIUM P1
Stub provider for CI HIGH (unlocks hermetic testing immediately) LOW P1
Zero-setup DB bootstrap HIGH (self-hoster operator UX) LOW P1
Self-service Google OAuth onboarding + token reconnect HIGH (wife's Google Calendar + non-technical UX) MEDIUM P1
Google Calendar read (unified view) HIGH (core v1.2 value proposition) HIGH P1
Google Calendar write-back HIGH (full parity with Fastmail) MEDIUM P1
Token-expiry reconnect banner + one-tap reconnect HIGH (non-technical Apple member must self-serve this) LOW P1
Multiple reminders per event MEDIUM (power user UX; v1.1 deferred this) MEDIUM P2
PWA dark mode (System/Light/Dark) MEDIUM (iOS Dark Mode users; polish) LOW P2
Provider health status indicator in Settings MEDIUM (ambient awareness without hunting menus) LOW P2
CI dependency updates (999.18) LOW (hygiene; not user-visible) LOW P3

Key UX Notes for Non-Technical Apple Member

The wife's experience is a hard constraint on every feature in this milestone:

  1. Google onboarding. Must be a single "Connect Google Calendar" tap that opens a recognizable Google consent screen — she knows what that looks like. The return flow back to FamilySync must work in Safari standalone PWA mode (same OIDC-redirect constraint validated in Phase 3). Do not require her to copy/paste anything.

  2. Token reconnect. When her Google token expires, she must see a banner in the calendar view — not discover it by noticing missing events days later. The banner says "Google Calendar disconnected" with a "Reconnect" button. One tap, done. No re-adding the account from scratch.

  3. Calendar picker on event creation. The picker must default to her most-used calendar; she should not have to think about it for routine events. The dropdown must use calendar names she recognizes ("Personal", "Family"), not provider-internal IDs or email addresses.

  4. Dark mode toggle. In Settings, labeled "Appearance: System / Light / Dark" — simple three-option choice, no color wheel. System should be the default (respects her iPhone's Dark Mode setting automatically).

  5. Multiple reminders. The add/remove reminder list must be tappable with one thumb on iPhone. The "Add reminder" link must meet the 44px minimum touch target. The list must not overflow off-screen on narrow viewports.


Sources


Feature research for: FamilySync v1.2 — multi-provider calendar, self-service onboarding, multiple reminders, dark mode, zero-setup DB, dev/CI stub provider Researched: 2026-06-19