docs: refresh project documentation against current codebase
Publish / publish (push) Successful in 26s

This commit is contained in:
Lucas Berger
2026-06-18 06:44:29 -04:00
parent 18d3ee6a4f
commit 1e2cc52659
11 changed files with 872 additions and 178 deletions
+58 -22
View File
@@ -10,7 +10,9 @@ Part of the [FamilySync monorepo](../../README.md).
- **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
- **Auth** — dual-mode: OIDC authorization-code + PKCE flow against Authelia (`@hono/oidc-auth`) for production; local username/password auth (scrypt, JWT session cookie) for no-OIDC or first-boot scenarios. `DEV_AUTH_BYPASS=true` skips both for local development
- **Setup wizard** — `/api/setup/*` surface guides first-run configuration of OIDC, VAPID keys, and member credentials before the app is locked
- **Admin** — role-gated `/api/admin/*` for member management, credential rotation, and calendar sharing designation
- **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
@@ -22,17 +24,27 @@ src/
routes/
events.ts CalDAV event CRUD endpoints
lists.ts List and list-item CRUD endpoints
me.ts Authenticated user profile endpoint
me.ts Authenticated user profile endpoint + OIDC-link initiation
push.ts Push subscription registration
sse.ts SSE stream for live list updates
health.ts Unauthenticated health check
setup.ts First-run setup wizard surface (/api/setup/*)
admin.ts Role-gated admin API (members, credentials, calendars)
localAuth.ts Local login/logout endpoints (/api/auth/local/*)
authMode.ts Pre-auth auth-mode discovery (/api/auth/mode)
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
middleware.ts oidcAuthMiddleware + processOAuthCallback + oidcConfigFallbackMiddleware
devBypass.ts DEV_AUTH_BYPASS passthrough (non-production only)
localAuthMiddleware.ts local-session cookie → c.get('user') middleware
localCredentials.ts scrypt password hashing and constant-time verification
localSession.ts HS256 JWT session-cookie issue / verify / clear helpers
linkNonceStore.ts Single-use nonce store for OIDC-link CSRF prevention
linkOidc.ts Atomic OIDC-identity binding + local credential removal
oidcConfig.ts Centralized OIDC config resolution (env OR app_config)
persistSessionCookie.ts Re-issues session cookie as persistent for PWA
user.ts User upsert on first login
broker/
@@ -40,11 +52,12 @@ src/
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
credentialSync.ts Shared validate→encrypt→store→initial-sync helper
sync.ts REPORT → ical.js → DB upsert logic
write.ts CalDAV PUT/DELETE helpers
expand.ts recurrence expansion via ICAL.RecurExpansion
expand.ts Recurrence expansion via ICAL.RecurExpansion
vevent.ts VEVENT ↔ DB row mapping
crypto.ts AES-256-GCM encrypt/decrypt for stored app passwords
crypto.ts AES-256-GCM encrypt/decrypt for stored app passwords (APP_PASSWORD_ENCRYPTION_KEY)
lib/
listEmitter.ts In-process EventEmitter for SSE fan-out
listChangeDispatcher.ts Publishes list mutations to listEmitter
@@ -53,6 +66,11 @@ src/
pushCoalescer.ts Debounces push for rapid successive edits
listAccess.ts List permission helpers
rank.ts Fractional indexing helpers
bootGuards.ts Boot-time env guards (blocks DEV_AUTH_BYPASS in production; enforces LOCAL_SESSION_SECRET)
setupGuard.ts isSetupLocked() — prevents re-running the wizard after completion
householdTimezone.ts Shared IANA timezone accessor with env fallback
outboxTrigger.ts In-process drain signal between routes and outboxWorker
requireAdmin.ts DB-enforced admin role middleware
```
## Running in the workspace
@@ -107,26 +125,44 @@ Migration files are written to `src/db/migrations/` and checked into source cont
## 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 |
| 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 |
| `OIDC_REDIRECT_URI` | No | Explicit redirect URI (overrides the `${OIDC_AUTH_EXTERNAL_URL}/callback` default) |
| `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 |
| `APP_PASSWORD_ENCRYPTION_KEY` | Yes | AES-256-GCM key (64-char hex) for stored Fastmail app passwords |
| `LOCAL_SESSION_SECRET` | Yes (local auth) | HS256 signing key for local-session JWT cookies (min 32 chars) |
| `LOCAL_SESSION_EXPIRES` | No (default `86400`)| Local session lifetime in seconds |
| `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 |
| `TZ` | No | IANA timezone fallback when household_timezone is not set in app_config |
> **Note:** `CREDENTIAL_ENCRYPTION_KEY` was renamed to `APP_PASSWORD_ENCRYPTION_KEY`. Update any existing `.env` files if upgrading from an earlier phase.
See [../../docs/CONFIGURATION.md](../../docs/CONFIGURATION.md) for the full reference.
## Authentication modes
The API supports two non-exclusive auth modes, determined at startup:
| Mode | When active | How it works |
| ---- | ----------- | ------------ |
| **Local** | Always (default) | `POST /api/auth/local/login` with username + password; issues an HS256 JWT `local-session` cookie. Requires `LOCAL_SESSION_SECRET`. |
| **OIDC** | When `OIDC_ISSUER` + `OIDC_CLIENT_ID` are set (env or app_config) | `@hono/oidc-auth` authorization-code + PKCE against Authelia. Local users can upgrade to OIDC via `POST /api/me/link-oidc`. |
| **Dev bypass** | `DEV_AUTH_BYPASS=true` in non-production | Skips both guards and injects a synthetic dev user. Blocked in `NODE_ENV=production` by boot guard. |
`GET /api/auth/mode` returns `{ localEnabled, oidcEnabled }` before authentication — the PWA uses this to decide which login form to show.
## Tests
Tests live in `tests/` (integration, route, broker unit) and `test/setup.ts` (global setup/teardown).
+23 -9
View File
@@ -32,13 +32,17 @@ The API backend must also be running for most features. See [GETTING-STARTED.md]
## 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`) |
| 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 lint` | Run ESLint over `src/` and `e2e/` with zero warnings allowed |
| `pnpm --filter @familysync/pwa typecheck` | Run `tsc --noEmit` for both `src/` and `e2e/` tsconfigs |
| `pnpm --filter @familysync/pwa test` | Run Vitest unit/integration suite once (`vitest run`) |
| `pnpm --filter @familysync/pwa test:e2e` | Run Playwright end-to-end tests headlessly |
| `pnpm --filter @familysync/pwa test:e2e:ui` | Open the Playwright UI runner |
| `pnpm --filter @familysync/pwa test:e2e:headed` | Run Playwright tests in a headed browser |
## Source layout
@@ -48,7 +52,7 @@ src/
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)
routes/ # React Router route components with co-located tests (AdminPage, ListDetail, ListsIndex, LoginPage, SetupPage)
store/ # Zustand stores: calendarStore, listsStore
styles/ # Global CSS
main.tsx # App entry point — React Query client, router, global error handlers
@@ -72,7 +76,7 @@ In development the Vite proxy routes `/api` requests to the API server on port 3
## 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`.
Unit and integration 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
@@ -82,6 +86,16 @@ pnpm --filter @familysync/pwa test
pnpm --filter @familysync/pwa exec vitest
```
End-to-end tests live in the `e2e/` directory and run with Playwright (`@playwright/test` 1.60.0). They cover login, calendar, lists, layout, admin, and timezone verification flows.
```bash
# headless
pnpm --filter @familysync/pwa test:e2e
# with Playwright UI
pnpm --filter @familysync/pwa test:e2e:ui
```
No coverage threshold is configured. Run `pnpm --filter @familysync/pwa typecheck` separately — Vitest uses esbuild and will not surface TypeScript errors.
## PWA install notes