14 KiB
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) — 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
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/ tsdavperformsPROPFINDon the principal to discover all calendar collections, then fetches each collection's events viaREPORT(calendar-query or calendar-multiget).- One app password covers all calendars owned by that account under the default "Mail, Contacts & Calendars" scope.
tsdavreturns raw iCalendar strings. Pass each toical.jsfor parsing into event objects, then userrulefor RRULE expansion into the date range the UI needs.- Write-back (create/edit/delete): PUT a new
.icsto 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/axiosagainst 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
mysql2directly — the same driver you'd use raw; no runtime translation layer - Drizzle's
mysqlTableschema is fully MariaDB-compatible (MariaDB is wire-compatible with MySQL; Drizzle'smysqldialect 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.invalidateQueriesfrom 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: S256token_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 — Confirmed CalDAV-only for calendars; JMAP calendars not available
- Fastmail App Passwords — Scope covers CalDAV; single password covers all calendars in account
- Using Fastmail with CalDAV libraries — Principal URL pattern, app password auth
- Fastmail Calendar Sharing — Sharing is multi-user-account scoped; cross-account sharing unconfirmed
- tsdav npm — Version 2.2.2 confirmed
- ical.js npm — Version 2.2.1 confirmed; Mozilla-maintained
- rrule npm — Version 2.8.1 confirmed
- Hono — Version 4.12.23; Node.js adapter confirmed
- Drizzle ORM MySQL — MariaDB via mysql2 confirmed
- vite-plugin-pwa — Version 1.3.0; Workbox 7 integration
- web-push npm — Version 3.6.7
- Meet Declarative Web Push — WebKit — Safari 18.4+, iOS 18.4+ confirmed
- PWA iOS Limitations 2026 — iOS 16.4 minimum; home screen required; EU DMA regression
- Authelia Express.js Integration — Authorization code + PKCE flow; client_secret_basic
- @hono/oidc-auth GitHub — Storage-less JWT session cookies; Version 1.8.3
Conventions
Conventions not yet established. Will populate as patterns emerge during development.
Architecture
Architecture not yet mapped. Follow existing patterns found in the codebase.
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 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-quickfor small fixes, doc updates, and ad-hoc tasks/gsd-debugfor investigation and bug fixing/gsd-execute-phasefor planned phase work
Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.
Developer Profile
Profile not yet configured. Run
/gsd-profile-userto generate your developer profile. This section is managed bygenerate-claude-profile-- do not edit manually.