docs: generate project documentation

This commit is contained in:
Lucas Berger
2026-06-10 18:17:51 -04:00
parent 581b31916b
commit a9c3304c4e
10 changed files with 2031 additions and 279 deletions
+151
View File
@@ -0,0 +1,151 @@
<!-- generated-by: gsd-doc-writer -->
# @familysync/api
The Hono backend for FamilySync. Acts as a calendar broker over Fastmail CalDAV, stores collaborative lists in MariaDB, enforces OIDC auth via Authelia, and delivers live list updates via SSE and push notifications via VAPID.
Part of the [FamilySync monorepo](../../README.md).
## What it does
- **Calendar broker** — polls Fastmail CalDAV every 5 minutes via `tsdav`; parses iCalendar payloads with `ical.js` and expands recurrence rules with `ical.js`'s `ICAL.RecurExpansion`; writes changes back to Fastmail through an outbox worker
- **Collaborative lists** — creates, reorders (fractional indexing), and syncs grocery/gift lists in MariaDB via Drizzle ORM
- **OIDC auth** — all `/api/*` routes protected by `@hono/oidc-auth` with authorization-code + PKCE flow against Authelia; `DEV_AUTH_BYPASS=true` skips OIDC for local development
- **Live sync** — Server-Sent Events stream list mutations to connected PWA clients in real time
- **Push notifications** — web-push (VAPID) delivers reminders for shared timed events to subscribed browsers
## Source layout
```
src/
index.ts Hono app entrypoint; server startup; background worker initialization
routes/
events.ts CalDAV event CRUD endpoints
lists.ts List and list-item CRUD endpoints
me.ts Authenticated user profile endpoint
push.ts Push subscription registration
sse.ts SSE stream for live list updates
health.ts Unauthenticated health check
db/
schema.ts Drizzle table definitions (MariaDB/mysql2)
client.ts Drizzle client singleton
migrations/ SQL migrations generated by drizzle-kit
auth/
middleware.ts oidcAuthMiddleware + processOAuthCallback
devBypass.ts DEV_AUTH_BYPASS passthrough (non-production only)
persistSessionCookie.ts Re-issues session cookie as persistent for PWA
user.ts User upsert on first login
broker/
poller.ts 5-minute setInterval CalDAV ctag change-detection
outboxWorker.ts 15-second drain of pending CalDAV writes to Fastmail
reminderScheduler.ts 1-minute scan for upcoming shared events → push
client.ts tsdav client factory
sync.ts REPORT → ical.js → DB upsert logic
write.ts CalDAV PUT/DELETE helpers
expand.ts recurrence expansion via ICAL.RecurExpansion
vevent.ts VEVENT ↔ DB row mapping
crypto.ts AES-256-GCM encrypt/decrypt for stored app passwords
lib/
listEmitter.ts In-process EventEmitter for SSE fan-out
listChangeDispatcher.ts Publishes list mutations to listEmitter
eventChangeDispatcher.ts Publishes calendar mutations
pushDispatcher.ts Dispatches VAPID push payloads
pushCoalescer.ts Debounces push for rapid successive edits
listAccess.ts List permission helpers
rank.ts Fractional indexing helpers
```
## Running in the workspace
All commands below run from the monorepo root via the `--filter` flag, or from `apps/api/` directly.
### Prerequisites
- Node.js 22 LTS
- `pnpm` (see root `package.json` for version)
- MariaDB reachable at the coordinates in your `.env`
- Authelia OIDC provider (or use `DEV_AUTH_BYPASS=true` for local development)
### Development
`dev` runs the compiled `dist/` with `node --watch`. You must build first — `tsc` output in `dist/` is the source of truth at runtime.
```bash
# From monorepo root:
pnpm --filter @familysync/api build # compile TypeScript → dist/
pnpm --filter @familysync/api dev # node --watch dist/index.js
# Or from apps/api/:
pnpm build
pnpm dev
```
Rebuild after any source change; `node --watch` reloads on `dist/` file changes but does not invoke `tsc` itself.
### Production
```bash
pnpm --filter @familysync/api build
pnpm --filter @familysync/api start # node dist/index.js
```
The server listens on port `3000`.
## Database migrations
Never use `drizzle-kit push` against a populated MariaDB instance — it emits false destructive diffs and will truncate data.
```bash
# 1. Generate SQL migration files from schema changes:
pnpm --filter @familysync/api db:generate
# 2. Apply pending migrations:
pnpm --filter @familysync/api db:migrate
```
Migration files are written to `src/db/migrations/` and checked into source control.
## Environment variables
| Variable | Required | Description |
|---|---|---|
| `DB_HOST` | Yes | MariaDB host |
| `DB_USER` | Yes | MariaDB user |
| `DB_PASSWORD` | Yes | MariaDB password |
| `DB_NAME` | Yes | MariaDB database name |
| `DB_PORT` | No (default `3306`) | MariaDB port |
| `OIDC_ISSUER` | Yes (production) | Authelia issuer URL |
| `OIDC_CLIENT_ID` | Yes (production) | OIDC client ID |
| `OIDC_CLIENT_SECRET` | Yes (production) | OIDC client secret |
| `OIDC_AUTH_EXTERNAL_URL` | Yes (production) | External-facing URL for redirect_uri behind Pangolin tunnel |
| `VAPID_SUBJECT` | Yes (push) | `mailto:` or `https:` operator identifier |
| `VAPID_PUBLIC_KEY` | Yes (push) | VAPID public key |
| `VAPID_PRIVATE_KEY` | Yes (push) | VAPID private key |
| `CREDENTIAL_ENCRYPTION_KEY` | Yes | AES-256-GCM key for stored Fastmail app passwords |
| `DEV_AUTH_BYPASS` | No | Set to `true` (non-production only) to skip OIDC and inject a dev user |
| `NODE_ENV` | No | Set to `production` to enforce OIDC unconditionally |
See [../../docs/CONFIGURATION.md](../../docs/CONFIGURATION.md) for the full reference.
## Tests
Tests live in `tests/` (integration, route, broker unit) and `test/setup.ts` (global setup/teardown).
```bash
# Run full suite (sequential — shared MariaDB requires serial file execution):
pnpm --filter @familysync/api test
# Watch mode:
pnpm --filter @familysync/api test:watch
# Type-check without emitting:
pnpm --filter @familysync/api typecheck
```
Integration tests that hit MariaDB require a running dev DB with `DB_HOST=127.0.0.1` and credentials from your `.env`. See [../../docs/TESTING.md](../../docs/TESTING.md) for the full setup.
## Further reading
- [Architecture](../../docs/ARCHITECTURE.md) — system overview and component diagram
- [API reference](../../docs/API.md) — endpoint table, request/response shapes, auth flow
- [Configuration](../../docs/CONFIGURATION.md) — all environment variables
- [Deployment](../../docs/deployment.md) — Docker Compose, Unraid setup, VAPID key generation
+97
View File
@@ -0,0 +1,97 @@
<!-- generated-by: gsd-doc-writer -->
# @familysync/pwa
The React 19 PWA frontend for FamilySync. Delivers a single installable, low-friction interface showing the color-coded family calendar and shared collaborative lists, designed for a mixed Android/Apple household.
Part of the [FamilySync monorepo](../../README.md).
## Stack
| Layer | Library | Version |
|-------|---------|---------|
| UI framework | React | ^19.0.0 |
| Build + dev server | Vite | 8.0.16 |
| PWA service worker + manifest | vite-plugin-pwa | ^1.3.0 |
| Calendar widget | @schedule-x/calendar | 4.6.0 |
| Server state | @tanstack/react-query | 5.101.0 |
| UI state | zustand | 5.0.14 |
| Routing | react-router | ^7.17.0 |
| iCalendar parsing | ical.js | 2.2.1 |
| Drag-and-drop (lists) | @dnd-kit/core + @dnd-kit/sortable | ^6 / ^10 |
## Development
Run the PWA dev server from the monorepo root:
```bash
pnpm --filter @familysync/pwa dev
```
The API backend must also be running for most features. See [GETTING-STARTED.md](../../docs/GETTING-STARTED.md) for full stack bring-up instructions.
## Scripts
| Command | What it does |
|---------|--------------|
| `pnpm --filter @familysync/pwa dev` | Start Vite dev server (HMR) |
| `pnpm --filter @familysync/pwa build` | Type-check then build production bundle (`tsc && vite build`) |
| `pnpm --filter @familysync/pwa preview` | Serve the production build locally |
| `pnpm --filter @familysync/pwa typecheck` | Run `tsc --noEmit` without emitting files |
| `pnpm --filter @familysync/pwa test` | Run Vitest test suite once (`vitest run`) |
## Source layout
```
src/
api/ # Typed fetch wrappers for @familysync/api (client.ts, listsClient.ts)
components/ # Shared UI components co-located with their *.test.tsx files
hooks/ # Custom React hooks (useListSSE, usePushSubscription, useFocusTrap)
lib/ # Pure helpers: calendarConfig, colorUtils, eventDateTime, hydrateEvents, loginRedirect
routes/ # React Router route components with co-located tests (ListDetail, ListsIndex)
store/ # Zustand stores: calendarStore, listsStore
styles/ # Global CSS
main.tsx # App entry point — React Query client, router, global error handlers
App.tsx # Root component
sw.ts # Workbox service worker entry
test-setup.ts # Vitest + jest-dom global setup
```
Routes and their co-located components own the feature slice. Library code (pure, side-effect-free) lives in `lib/`. Zustand stores hold UI-only state; server data is exclusively managed by TanStack Query.
## Communication with the API
All backend calls go through `src/api/client.ts` and `src/api/listsClient.ts`. Key behavior:
- `credentials: 'include'` on every request so the OIDC session cookie is forwarded.
- `redirect: 'manual'` — a 401 or opaque redirect (the Authelia 302) is caught and thrown as a typed `SessionExpiredError`. The global `QueryCache` / `MutationCache` error handler in `main.tsx` intercepts this and shows the session-expiry interstitial.
- Re-authentication requires a top-level navigation to `/api/login` (handled by `src/lib/loginRedirect.ts`), not a fetch redirect, because browsers block CORS redirects to an external IdP.
- Live list updates are delivered via SSE through `src/hooks/useListSSE.ts`; the hook calls `queryClient.invalidateQueries` on each event so TanStack Query re-fetches.
In development the Vite proxy routes `/api` requests to the API server on port 3000, keeping cookies same-site. In production both apps are served same-origin via the Pangolin/Newt tunnel.
## Testing
Tests are co-located with their source files (`*.test.tsx` / `*.test.ts`) and use React Testing Library + `@testing-library/jest-dom`. The test environment is `jsdom`.
```bash
# run once
pnpm --filter @familysync/pwa test
# watch mode (during development)
pnpm --filter @familysync/pwa exec vitest
```
No coverage threshold is configured. Run `pnpm --filter @familysync/pwa typecheck` separately — Vitest uses esbuild and will not surface TypeScript errors.
## PWA install notes
- The app must be added to the Home Screen on iOS for push notifications to work (iOS 16.4+ minimum).
- Push permission must be requested inside a tap handler; calling `pushManager.subscribe()` on page load is blocked.
- Background sync and background push are not supported on iOS; all push messages must display a visible notification.
## Further reading
- [Architecture overview](../../docs/ARCHITECTURE.md)
- [Getting started](../../docs/GETTING-STARTED.md)
- [Development guide](../../docs/DEVELOPMENT.md)
- [Testing guide](../../docs/TESTING.md)