<!-- GSD:project-start source:PROJECT.md -->

## Project

**FamilySync**

FamilySync is a self-hosted, Dockerized family organization hub for a two-person household, modeled loosely on Skylight. It gives the family one unified, color-coded calendar and shared collaborative lists (groceries, gift ideas), reachable from a single low-friction React PWA — across a mixed Android/Fastmail and Apple ecosystem — built as one custom app on top of the family's existing paid Fastmail account. Email stays with members' existing mail clients and is out of scope.

**Core Value:** The household can see and co-edit one color-coded family calendar (shared + each member's personal) and shared lists from a single low-friction PWA — cross-ecosystem, no app store, no per-member calendar credential juggling.

### Constraints

- **Tech stack**: MariaDB for the database — PostgreSQL is not available in the stack
- **Tech stack**: Redis available (optional, for live list sync / push)
- **Infrastructure**: Unraid host running Docker + Docker Compose
- **Auth**: Authelia (already deployed) + Local Auth — OIDC/OAuth2 for the custom app; all members authenticate through it
- **Calendar backend**: Fastmail (paid, existing) is the single source for all calendars via JMAP/CalDAV
- **Frontend**: React PWA only — no React Native, no App Store
- **Networking**: Split-DNS internal domain, private IPs internally; public exposure via Pangolin/Newt tunnel, no open ports
- **UX**: Must be slick and low-friction for a non-technical Apple member — hard constraint, drives design tradeoffs
- **Scope**: No email features; no forking existing apps

<!-- GSD:project-end -->

<!-- GSD:stack-start source:research/STACK.md -->

## Technology Stack

## Recommended Stack

### Core Technologies

| Technology           | Version | Purpose                   | Why Recommended                                                                                                                                                             |
| -------------------- | ------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Node.js + TypeScript | 22 LTS  | Backend runtime           | First-class typing, same language as frontend, largest CalDAV/OIDC library ecosystem                                                                                        |
| Hono                 | 4.12.23 | HTTP framework            | Web-Standards-native, first-class TypeScript, built-in SSE helper, WebSocket via `@hono/node-server`; lighter than Express and better ergonomics than Fastify for this size |
| Drizzle ORM          | 0.45.2  | MariaDB query layer       | Type-safe SQL, zero runtime overhead, native `mysql2` driver support, schema-as-code migrations via `drizzle-kit`                                                           |
| mysql2               | 3.22.4  | MariaDB driver            | The only maintained native MariaDB/MySQL driver; Drizzle targets it explicitly                                                                                              |
| React 19             | 19.x    | PWA frontend              | Required by project; concurrent features, stable                                                                                                                            |
| Vite                 | 8.0.x   | Build tooling             | De-facto standard for React PWAs; fast HMR, native ESM                                                                                                                      |
| vite-plugin-pwa      | 1.3.0   | Service worker + manifest | Zero-config Workbox integration, handles install prompt, offline cache, background sync scaffolding                                                                         |

### Supporting Libraries

| Library               | Version | Purpose                          | When to Use                                                                                                                                   |
| --------------------- | ------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| tsdav                 | 2.2.2   | CalDAV client for Node.js        | All calendar reads and writes against Fastmail CalDAV endpoint; handles PROPFIND, REPORT, PUT, DELETE                                         |
| ical.js               | 2.2.1   | iCalendar (.ics) parsing         | Parse raw VCALENDAR/VEVENT payloads returned by tsdav; handles VTIMEZONE, RDATE, EXDATE                                                       |
| rrule                 | 2.8.1   | Recurrence rule expansion        | Expand RRULE strings into concrete event occurrences for the calendar view; ical.js's built-in expansion is less ergonomic for UI consumption |
| web-push              | 3.6.7   | Server-side VAPID push           | Generate VAPID keys, sign and dispatch push messages to browser push services (APNs for iOS, FCM for Android)                                 |
| @hono/oidc-auth       | 1.8.3   | OIDC session middleware for Hono | Storage-less JWT session cookies; authorization-code + PKCE flow; works with any RFC-compliant OIDC provider including Authelia               |
| openid-client         | 6.8.4   | Low-level OIDC primitives        | If `@hono/oidc-auth` proves insufficient (e.g., custom token introspection), use this as the lower-level escape hatch                         |
| ioredis               | 5.11.0  | Redis client                     | Pub/sub for broadcasting list-change events to SSE connections across Node processes                                                          |
| zod                   | 3.24.x  | Schema validation                | Validate API request bodies and CalDAV event payloads before writing back to Fastmail                                                         |
| @hono/zod-validator   | 0.8.0   | Hono middleware for Zod          | Validate request body/query in route handlers with Zod schemas                                                                                |
| @tanstack/react-query | 5.101.0 | Server state + caching           | Manages calendar and list data fetching, background refetch, stale-while-revalidate; pairs with SSE for live list updates                     |
| zustand               | 5.0.14  | Client state                     | UI-only state (selected date range, color assignments, drawer open/closed); keep server state in React Query                                  |
| drizzle-kit           | 0.31.10 | Schema migrations                | Generates and runs MariaDB migrations from Drizzle schema definitions                                                                         |

### Development Tools

| Tool              | Purpose                                 | Notes                                                                          |
| ----------------- | --------------------------------------- | ------------------------------------------------------------------------------ |
| TypeScript 5.x    | Strict typing across backend + frontend | `strict: true`; share types between packages via a `packages/shared` workspace |
| ESLint + Prettier | Lint + format                           | Standard config; no bikeshedding needed                                        |
| Docker Compose    | Local dev + production parity           | Match Unraid stack exactly in dev                                              |
| Vitest            | Unit + integration tests                | Vite-native, same config as frontend                                           |

## Installation

# Backend

# Frontend

# Dev

## Calendar Integration: CalDAV, Not JMAP

- Principal URL: `https://caldav.fastmail.com/dav/principals/user/broker@fastmail.com/`
- `tsdav` performs `PROPFIND` on the principal to discover all calendar collections, then fetches each collection's events via `REPORT` (calendar-query or calendar-multiget).
- One app password covers all calendars owned by that account under the default "Mail, Contacts & Calendars" scope.
- `tsdav` returns raw iCalendar strings. Pass each to `ical.js` for parsing into event objects, then use `rrule` for RRULE expansion into the date range the UI needs.
- Write-back (create/edit/delete): PUT a new `.ics` to the collection URL; DELETE by UID.
- `node-ical`: older fork with weaker RRULE support; ical.js is maintained by Mozilla and is the reference implementation
- Direct `fetch`/`axios` against CalDAV: re-inventing XML namespace handling and PROPFIND parsing; tsdav exists specifically to avoid this
- JMAP: not available for calendars on Fastmail today

## Backend Framework

- First-class TypeScript with RPC-style type sharing (Hono RPC can export typed client for the React frontend — eliminates API drift)
- Built-in SSE streaming helper (`streamSSE`) for live list updates
- WebSocket support via `@hono/node-server`
- Runs on Node.js 22 LTS in Docker with `@hono/node-server`
- Prisma generates a binary engine that adds complexity in Docker images and has weaker MariaDB compatibility signals
- Drizzle uses `mysql2` directly — the same driver you'd use raw; no runtime translation layer
- Drizzle's `mysqlTable` schema is fully MariaDB-compatible (MariaDB is wire-compatible with MySQL; Drizzle's `mysql` dialect works)
- Type inference from schema → query results is the core value proposition; zero runtime overhead

## React PWA Stack

- TanStack Query owns all server-side state: calendar events, lists, user profile. It handles background refetch, cache invalidation, and loading states. Use `queryClient.invalidateQueries` from SSE event handlers to keep list data live.
- Zustand owns pure UI state: selected month, color assignments per calendar, drawer states. Do not put server data in Zustand.

| Requirement           | Detail                                                                                 |
| --------------------- | -------------------------------------------------------------------------------------- |
| Minimum iOS version   | 16.4 — push is silently unavailable on earlier versions                                |
| Installation required | PWA **must** be added to Home Screen; push does not work from Safari browser tabs      |
| User gesture          | `pushManager.subscribe()` must be called inside a tap handler, not on page load        |
| EU users on iOS 17.4+ | PWAs may open in Safari tabs instead of standalone mode due to DMA; affects push reach |
| Silent push           | Not supported on iOS; all push messages must display a visible notification            |
| Background sync       | Not supported on iOS; no `BackgroundSync` or `PeriodicBackgroundSync`                  |

## Authelia OIDC Integration

- `response_types: [code]`
- `grant_types: [authorization_code, refresh_token]`
- `require_pkce: true`, `pkce_challenge_method: S256`
- `token_endpoint_auth_method: client_secret_basic`

## Live List Sync

## Alternatives Considered

| Recommended     | Alternative            | Why Not                                                                                           |
| --------------- | ---------------------- | ------------------------------------------------------------------------------------------------- |
| Hono            | Express                | No native TypeScript ergonomics; no built-in SSE; larger ecosystem but more boilerplate           |
| Hono            | Fastify                | Good choice but heavier plugin model; Hono's Web Standards alignment is better for this size      |
| Drizzle         | Prisma                 | Binary engine complicates Docker; weaker explicit MariaDB support; heavier                        |
| tsdav           | Raw fetch + xml2js     | CalDAV XML namespace handling is tedious; tsdav is the established TypeScript CalDAV client       |
| ical.js         | node-ical              | node-ical is a fork that has diverged; ical.js is the Mozilla-maintained reference implementation |
| @hono/oidc-auth | express-openid-connect | express-openid-connect is Express-specific; Hono middleware is the correct fit                    |
| SSE             | WebSockets             | WebSockets are bidirectional; list sync is server→client only; SSE is simpler and proxy-friendly  |
| CalDAV          | JMAP                   | JMAP calendars not available on Fastmail as of 2026                                               |

## What NOT to Use

| Avoid                       | Why                                                              | Use Instead      |
| --------------------------- | ---------------------------------------------------------------- | ---------------- |
| JMAP for calendars          | Not implemented by Fastmail; spec not finalized                  | CalDAV via tsdav |
| Prisma                      | Binary engine, weaker MariaDB compat, larger footprint in Docker | Drizzle ORM      |
| oidc-client-ts              | Browser-side OIDC library; wrong layer for a backend-session app | @hono/oidc-auth  |
| node-ical                   | Older fork of ical.js, less maintained, weaker RRULE handling    | ical.js          |
| Create React App            | Deprecated February 2025                                         | Vite             |
| PostgreSQL                  | Not in the Unraid stack; hard constraint                         | MariaDB          |
| NestJS                      | Massive framework overhead for a two-user household app          | Hono             |
| Firebase/FCM as push broker | Third-party dependency; VAPID direct push works without it       | web-push (VAPID) |

## Version Compatibility

| Package               | Compatible With        | Notes                                                                                      |
| --------------------- | ---------------------- | ------------------------------------------------------------------------------------------ |
| drizzle-orm@0.45.x    | mysql2@3.x             | Use `drizzle-orm/mysql2` import path; mysql2@3.x uses Promises API by default              |
| vite-plugin-pwa@1.3.x | Vite@8.x, Workbox@7.x  | vite-plugin-pwa 0.16+ requires Node 16+; 1.x tracks Vite 6+                                |
| @hono/oidc-auth@1.8.x | hono@4.x, oauth4webapi | Peer-depends on hono 4.x                                                                   |
| ical.js@2.x           | rrule@2.8.x            | Use together: ical.js parses the RRULE string, pass to `new RRule(RRule.parseString(...))` |
| web-push@3.6.x        | Node.js 18+            | VAPID uses Web Crypto; works in Node.js 18+ natively                                       |

## Open Questions Flagged for Phase Research

## Sources

- [Fastmail API Documentation](https://www.fastmail.com/dev/) — Confirmed CalDAV-only for calendars; JMAP calendars not available
- [Fastmail App Passwords](https://www.fastmail.help/hc/en-us/articles/360058752854-App-passwords) — Scope covers CalDAV; single password covers all calendars in account
- [Using Fastmail with CalDAV libraries](https://utf9k.net/blog/fastmail-caldav/) — Principal URL pattern, app password auth
- [Fastmail Calendar Sharing](https://www.fastmail.help/hc/en-us/articles/1500000279781-Sharing-calendars-with-other-users) — Sharing is multi-user-account scoped; cross-account sharing unconfirmed
- [tsdav npm](https://www.npmjs.com/package/tsdav) — Version 2.2.2 confirmed
- [ical.js npm](https://www.npmjs.com/package/ical.js) — Version 2.2.1 confirmed; Mozilla-maintained
- [rrule npm](https://www.npmjs.com/package/rrule) — Version 2.8.1 confirmed
- [Hono](https://hono.dev/) — Version 4.12.23; Node.js adapter confirmed
- [Drizzle ORM MySQL](https://orm.drizzle.team/docs/get-started-mysql) — MariaDB via mysql2 confirmed
- [vite-plugin-pwa](https://vite-pwa-org.netlify.app/) — Version 1.3.0; Workbox 7 integration
- [web-push npm](https://www.npmjs.com/package/web-push) — Version 3.6.7
- [Meet Declarative Web Push — WebKit](https://webkit.org/blog/16535/meet-declarative-web-push/) — Safari 18.4+, iOS 18.4+ confirmed
- [PWA iOS Limitations 2026](https://www.magicbell.com/blog/pwa-ios-limitations-safari-support-complete-guide) — iOS 16.4 minimum; home screen required; EU DMA regression
- [Authelia Express.js Integration](https://www.authelia.com/integration/openid-connect/clients/expressjs/) — Authorization code + PKCE flow; client_secret_basic
- [@hono/oidc-auth GitHub](https://github.com/honojs/middleware/tree/main/packages/oidc-auth) — Storage-less JWT session cookies; Version 1.8.3

<!-- GSD:stack-end -->

<!-- GSD:conventions-start source:CONVENTIONS.md -->

## Conventions

### Browser-based verification

- **Use the `playwright-cli` skill (`.claude/skills/playwright-cli/`) to validate UI and workflows instead of asking the operator to check manually.** When a change touches the PWA, or a UI/UX decision needs grounding in real behavior, drive a real browser with `playwright-cli` and observe — don't prompt the human to do it. This applies to executors and verifiers too: prefer an automated `playwright-cli` check over a `checkpoint:human-verify` task whenever the check runs in a desktop/Chromium browser.
- **Exception — genuinely device-only checks still need a human.** iOS-Safari standalone-PWA behavior (Home-Screen install, standalone-mode OIDC redirect, iOS push) cannot be driven by `playwright-cli`; keep those as human checkpoints (e.g. Phase 3 Gate 2 iOS items).
- The `playwright-cli` binary is global (`/usr/local/bin/playwright-cli`). `@playwright/test` is not a repo dependency — install it in `apps/pwa` only if you need the spec-driven test-generation references.

<!-- GSD:conventions-end -->

<!-- GSD:architecture-start source:ARCHITECTURE.md -->

## Architecture

FamilySync is a pnpm monorepo with two apps: `apps/api` (Hono 4.x on Node 22 LTS, Drizzle ORM + MariaDB 11, TypeScript) and `apps/pwa` (React 19 + Vite 8 + vite-plugin-pwa). A single Docker Compose stack runs the API container (which also serves the PWA static build) and a MariaDB container, exposed through a Pangolin/Newt tunnel.

The backend handles two auth paths: local username/password (scrypt + HS256 JWT `local-session` cookie) and Authelia OIDC (authorization code + PKCE via `@hono/oidc-auth`). Both populate `c.get('user')`; the OIDC guard is skipped when a valid local session is present. A local user may link an OIDC identity later.

Calendar data lives exclusively in Fastmail CalDAV. The broker layer (`apps/api/src/broker/`) uses `tsdav` for PROPFIND/REPORT/PUT/DELETE, `ical.js` for VCALENDAR parsing, and `rrule` for server-side recurrence expansion. Writes are enqueued in a `calendarOutbox` table and drained asynchronously every 15 seconds; a ctag-based poller re-syncs calendars every 5 minutes.

Lists are persisted in MariaDB. Live list updates flow over SSE (`text/event-stream`) via an in-process Node.js `EventEmitter`; a 30-second polling fallback is always active. Push notifications (reminders + calendar change alerts) are dispatched via `web-push` (VAPID) to APNs/FCM. Redis is present in the stack but not yet used at runtime (reserved for future multi-process pub/sub).

The PWA uses TanStack Query for all server state (events, lists, user, sync status, auth mode) and Zustand for UI-only state (selected date, open panels, active tab).

```text
familysync/
├── apps/
│   ├── api/src/
│   │   ├── index.ts        # App entry: mounts routes, starts background workers
│   │   ├── routes/         # HTTP handlers (events, lists, me, push, sse, auth, admin, setup)
│   │   ├── auth/           # Local session + OIDC middleware + dev-bypass + OIDC-link
│   │   ├── broker/         # CalDAV client, sync, poller, outbox worker, RRULE expand, write
│   │   ├── db/             # Drizzle schema, mysql2 pool, migrations
│   │   └── lib/            # List/event emitters, push dispatcher, rank, guards, admin/setup helpers
│   └── pwa/src/
│       ├── App.tsx          # BrowserRouter shell
│       ├── routes/          # Page-level components
│       ├── components/      # Shared UI components
│       ├── api/             # Typed fetch wrappers (client.ts, listsClient.ts)
│       ├── hooks/           # useListSSE, usePushSubscription
│       ├── store/           # Zustand stores (calendarStore, listsStore)
│       └── sw.ts            # Custom Workbox service worker
├── docker-compose.yml       # Production stack (api + mariadb + redis)
└── docker-compose.dev.yml   # Dev overrides
```

See `docs/ARCHITECTURE.md` for the full Mermaid component diagram, data-flow walkthroughs, and key abstractions table.

<!-- GSD:architecture-end -->

<!-- GSD:skills-start source:skills/ -->

## Project Skills

No project skills found. Add skills to any of: `.claude/skills/`, `.agents/skills/`, `.cursor/skills/`, `.github/skills/`, or `.codex/skills/` with a `SKILL.md` index file.

<!-- GSD:skills-end -->

<!-- GSD:workflow-start source:GSD defaults -->

## GSD Workflow Enforcement

Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.

Use these entry points:

- `/gsd-quick` for small fixes, doc updates, and ad-hoc tasks
- `/gsd-debug` for investigation and bug fixing
- `/gsd-execute-phase` for planned phase work

Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.

<!-- GSD:workflow-end -->

<!-- GSD:profile-start -->

## Developer Profile

> Generated by GSD from session_analysis. Run `/gsd-profile-user` to update.

| Dimension      | Rating                | Confidence |
| -------------- | --------------------- | ---------- |
| Communication  | conversational        | MEDIUM     |
| Decisions      | fast-intuitive        | MEDIUM     |
| Explanations   | concise               | MEDIUM     |
| Debugging      | diagnostic            | MEDIUM     |
| UX Philosophy  | design-conscious      | MEDIUM     |
| Vendor Choices | opinionated           | LOW        |
| Frustrations   | instruction-adherence | MEDIUM     |
| Learning       | self-directed         | MEDIUM     |

**Directives:**

- **Communication:** Respond in a natural, conversational register. Expect messages that bundle multiple observations and a directive together -- address each point. Match brevity for short imperative confirmations, but engage the reasoning when the developer thinks aloud.
- **Decisions:** Present options concisely and expect a fast decision. Use clearly enumerated choices so the developer can triage them in one pass. Do not over-deliberate or request repeated confirmation -- move forward once a disposition is given.
- **Explanations:** Give brief explanations focused on the key decision or the 'why this is expected', then the change. Assume the developer reads and understands the implementation. When they ask 'is this fine/correct', answer the specific concern directly rather than expanding into a full tutorial.
- **Debugging:** When debugging, diagnose the root cause before patching and explain what caused the behavior. The developer supplies reproduction detail and often a partial theory -- confirm or refute it directly and pull real evidence (logs, actual config) rather than guessing.
- **UX Philosophy:** Treat UI/UX polish as first-class work, not deferred cleanup. Get layout, copy, placement, and visual feel right during implementation. Aim for a warm, friendly, low-friction aesthetic (rounded, comfortable, 'at home'). Flag and fix UI bugs (centering, navigation, missing controls) proactively.
- **Vendor Choices:** Respect the already-decided stack and the developer's stated infrastructure choices -- do not propose swapping established tools. When a new library or API is in question, expect the developer to want it validated rather than taken on faith. Confirm whether this matches their general tool-selection preference.
- **Frustrations:** Follow standing requirements exactly, especially passing local CI gates (prettier, eslint, gitleaks/secret scan, typecheck) BEFORE pushing -- this is a recurring pain point. When the developer states a fact about their environment, accept it and do not argue from an outdated model. Apply the instructed fix directly rather than re-litigating causes already understood.
- **Learning:** Assume the developer experiments and investigates independently. Answer specific targeted questions precisely rather than offering unsolicited walkthroughs. When introducing something new, point to the concrete thing to check or run so they can verify it hands-on themselves.

<!-- GSD:profile-end -->
