263 lines
22 KiB
Markdown
263 lines
22 KiB
Markdown
<!-- 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
|
|
|
|
> Profile not yet configured. Run `/gsd-profile-user` to generate your developer profile.
|
|
> This section is managed by `generate-claude-profile` -- do not edit manually.
|
|
|
|
<!-- GSD:profile-end -->
|