Files
familysync/CLAUDE.md
T
2026-06-18 21:06:32 -04:00

24 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
  • 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

Technology 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
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

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.

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.

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).

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)
└── docker-compose.dev.yml   # Dev overrides

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

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-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.

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.