Files
familysync/docs/DEVELOPMENT.md
T
Lucas Berger 9b62887f0f feat(20-03): unify member editor + declutter admin members panel
- playwright-cli verified: Members tab shows tappable rows, no retired buttons
- Row tap opens 'Edit member' sheet; per-section saves keep sheet open
- 'Add member' trigger opens 'Add member' sheet in create mode
- Profile save fires 'Profile saved.' toast; sheet stays open (D-05)
- eslint + prettier + typecheck + vitest (275 tests) all pass
- Fix pre-existing prettier drift in docs/*, CLAUDE.md, README.md, api/admin.ts
2026-06-18 17:39:00 -04:00

14 KiB

Development Guide

Local development setup and workflows for FamilySync — a pnpm monorepo with two workspaces: apps/api (Hono + Node.js) and apps/pwa (React + Vite).

Repo Layout

familysync/
├── apps/
│   ├── api/          # Hono API server — Node.js 22, TypeScript, Drizzle/MariaDB
│   └── pwa/          # React 19 PWA — Vite, TanStack Query, Schedule-X
├── docker-compose.yml
├── docker-compose.dev.yml
├── package.json      # Root workspace scripts
└── pnpm-workspace.yaml

Key paths inside apps/api/src/:

src/
├── db/
│   ├── schema.ts       # Drizzle table definitions (source of truth for migrations)
│   ├── client.ts       # mysql2 pool + drizzle instance
│   └── migrations/     # Generated SQL migration files
├── routes/             # Hono route files (events, lists, push, sse, me, health)
├── auth/               # OIDC middleware
├── broker/             # CalDAV broker (tsdav + ical.js)
└── lib/                # Shared utilities

Prerequisites

  • Node.js 22 LTS — the Dockerfile base is node:22-alpine; match this locally
  • pnpm 11.5.1 — managed via corepack (corepack enable pnpm)
  • Docker + Docker Compose — for MariaDB and Redis in dev
  • TypeScript 5.x — installed per-workspace as a dev dependency

Local Setup

1. Install dependencies

pnpm install

This installs all workspace packages (apps/api and apps/pwa) in a single pass.

2. Start the dev database and Redis

docker compose -f docker-compose.yml -f docker-compose.dev.yml up mariadb redis -d

The dev override (docker-compose.dev.yml) exposes MariaDB on localhost:3306 and Redis on localhost:6379.

3. Configure environment variables

Copy the root .env.example to .env and fill in the required values. The root .env is sourced by docker-compose for container env vars. When running the API server directly on the host (outside Docker), you must override DB_HOST:

# Source the root .env, then override DB_HOST for host-side execution
DB_HOST=127.0.0.1 node dist/index.js

Or set DB_HOST=127.0.0.1 in your local .env for the dev workflow. The docker-compose production config sets DB_HOST: mariadb (the service name); that value does not resolve on the host.

For auth bypass during local UI development, set DEV_AUTH_BYPASS=true — this skips OIDC and authenticates as the dev user (id 1).

Development Workflow

API dev loop

The dev script runs the compiled output via node --watch. A build must exist before starting the dev server — the watcher restarts dist/index.js on file changes, but it does not recompile TypeScript. You must rebuild when source changes.

# From apps/api — or use the root shortcut
pnpm --filter @familysync/api build   # compile src/ → dist/
pnpm --filter @familysync/api dev     # node --watch dist/index.js

# Root shortcuts
pnpm dev:api   # runs dev in apps/api (requires dist/ to already exist)

Recommended inner loop: run pnpm --filter @familysync/api build after each change, the --watch process restarts automatically.

PWA dev server

pnpm --filter @familysync/pwa dev
# or from root:
pnpm dev:pwa

Vite serves the PWA with HMR on the configured dev port. The PWA's API calls target the backend; set VITE_API_URL (or the Vite proxy config) to point at the running API.

Build Commands

Root workspace scripts

Command Description
pnpm dev:api Start API dev watcher (node --watch dist/index.js)
pnpm dev:pwa Start Vite dev server for the PWA
pnpm build Build both apps/api (tsc) and apps/pwa (tsc + vite build)
pnpm test Run API test suite (vitest run in apps/api)
pnpm test:e2e Run Playwright e2e harness (apps/pwa)
pnpm lint ESLint across all workspaces (pnpm -r --if-present lint)
pnpm format Reformat all files with Prettier (prettier --write .)
pnpm format:check Check formatting without writing (prettier --check .)
pnpm typecheck tsc --noEmit in all workspaces
pnpm md:lint Markdown lint (markdownlint-cli2) across the repo
pnpm generate-secrets Generate VAPID and session secret values via scripts/generate-secrets.mjs

apps/api scripts

Command Description
pnpm --filter @familysync/api build Compile TypeScript (tsc) → dist/
pnpm --filter @familysync/api dev Start node --watch dist/index.js
pnpm --filter @familysync/api start Start node dist/index.js (no watch)
pnpm --filter @familysync/api test Run vitest once (vitest run)
pnpm --filter @familysync/api test:watch Run vitest in watch mode
pnpm --filter @familysync/api lint ESLint src/ and tests/ (--max-warnings 0)
pnpm --filter @familysync/api typecheck tsc --noEmit
pnpm --filter @familysync/api db:generate Generate SQL migrations from schema changes
pnpm --filter @familysync/api db:migrate Apply pending migrations to the database

apps/pwa scripts

Command Description
pnpm --filter @familysync/pwa dev Start Vite dev server with HMR
pnpm --filter @familysync/pwa build tsc && vite builddist/
pnpm --filter @familysync/pwa preview Serve the production build locally
pnpm --filter @familysync/pwa lint ESLint src/ and e2e/ (--max-warnings 0)
pnpm --filter @familysync/pwa typecheck tsc --noEmit + tsc --project tsconfig.e2e.json --noEmit
pnpm --filter @familysync/pwa test Run vitest once
pnpm --filter @familysync/pwa test:e2e Run Playwright e2e tests
pnpm --filter @familysync/pwa test:e2e:headed Playwright in headed mode (visible browser)
pnpm --filter @familysync/pwa test:e2e:ui Playwright UI mode

Code Quality — Run Before Every Push

CI gates every PR to main on these checks. Run them locally before pushing to avoid a CI round-trip.

pnpm lint          # ESLint --max-warnings 0 across apps/api (src/ + tests/) and apps/pwa (src/ + e2e/)
pnpm format:check  # Prettier formatting check (use `pnpm format` to auto-fix)
pnpm typecheck     # tsc --noEmit in both apps (includes apps/pwa tsconfig.e2e.json)
pnpm md:lint       # Markdown lint (also runs in CI fast-checks)

ESLint

Config: eslint.config.js (root, flat ESLint 9 format). The config covers:

  • All apps/**/\*.{ts,tsx}js.configs.recommended+tseslint.configs.recommendedTypeCheckedwithprojectService: true(type-aware rules, auto-discovers alltsconfig.json files)
  • apps/pwa/**/\*.{ts,tsx}additionallyeslint-plugin-react+eslint-plugin-react-hooks (React 19 flat config; React Compiler rules disabled — this codebase does not use the Compiler)
  • All apps/**/\*.{ts,tsx}eslint-plugin-security(14 of 15 rules at error;detect-object-injection disabled due to high false-positive rate on schema-derived numeric keys)
  • Tool configs + test dirs (drizzle.config.ts, vitest.config.ts, apps/api/tests/**, apps/pwa/e2e/**) — type-aware rules disabled via disableTypeChecked (these files are outside the main tsconfig projects)
  • Prettier integrationeslint-config-prettier last in the config disables all formatting rules that conflict with Prettier

--max-warnings 0 is enforced: warnings count as failures. Blanket eslint-disable comments are not permitted — every suppression requires a justification comment.

Prettier

Config: .prettierrc (root). Settings: singleQuote: true, semi: true, tabWidth: 2, trailingComma: "all", printWidth: 100.

pnpm format        # write fixes in place
pnpm format:check  # check only (used in CI)

The .prettierignore file at the repo root excludes build output and generated files.

TypeScript Strict Checks

Both workspaces use "strict": true in their tsconfig.json. The build step (tsc) catches type errors in apps/api (since it emits output). For apps/pwa, Vite uses esbuild to transpile and does not perform type checking — vitest will pass even when there are type errors in the PWA. Always run the typecheck script explicitly:

# Check both workspaces
pnpm typecheck

# Or individually
pnpm --filter @familysync/api typecheck
pnpm --filter @familysync/pwa typecheck   # also checks tsconfig.e2e.json

Run pnpm typecheck before opening a PR to catch errors that vitest and Vite builds will silently miss.

CI Pipeline Overview

Every PR to main runs through .gitea/workflows/ci.yml. A changes path-filter job determines whether code files changed; the api and harness jobs are skipped entirely for doc-only PRs (changes only to .planning/**, .gitea/**, or *.md files).

Job Runs on Checks
fast-checks Every PR pnpm lintpnpm format:checkpnpm md:lintpnpm typecheckpnpm --filter @familysync/pwa test
api Code-change PRs only DB migrations + pnpm --filter @familysync/api test (vitest against a MariaDB 11 service container)
harness Code-change PRs only DB migrations + seed dev user + API build + Playwright e2e (WebKit + Chromium) with DEV_AUTH_BYPASS=true
security Every PR Gitleaks secret scan (PR diff); pnpm audit (High+Critical blocking) + outdated report on code-change PRs
gate Always Final aggregator — requires fast-checks and security to succeed; api and harness may be skipped

All five jobs must pass (or be legitimately skipped) before a PR can merge. See docs/TESTING.md for test suite details.

Drizzle Migration Workflow

Schema changes follow a strict two-step process. drizzle-kit push is not available — it has been removed from the scripts because it emits a false destructive diff (table truncation) on populated MariaDB databases.

Step 1 — Generate the migration

After editing apps/api/src/db/schema.ts:

pnpm --filter @familysync/api db:generate

This runs drizzle-kit generate and writes a new .sql file to apps/api/src/db/migrations/. Review the generated SQL before proceeding — confirm it matches the intended schema change with no unexpected DROP or truncation statements.

Step 2 — Apply the migration

pnpm --filter @familysync/api db:migrate

This runs drizzle-kit migrate and applies any pending migration files to the target database. Drizzle reads DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, and optionally DB_PORT (default 3306) from the environment, as defined in apps/api/drizzle.config.ts.

When running migrations from the host against the Docker database:

DB_HOST=127.0.0.1 pnpm --filter @familysync/api db:migrate

Migration files live in apps/api/src/db/migrations/ and are committed to version control.

Docker Compose Dev Stack

# Bring up the full dev stack (API in Docker + MariaDB + Redis, with ports exposed)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up

# Bring up only backing services (run API on host for faster iteration)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up mariadb redis -d

The dev override:

  • Exposes MariaDB on localhost:3306
  • Exposes Redis on localhost:6379
  • Mounts apps/api/src into the container for live source access
  • Sets NODE_ENV=development

The production docker-compose.yml builds the API and PWA into a single image (production target in apps/api/Dockerfile). The PWA dist/ is copied into the API image's ./public directory and served on port 3000.

Common Issues

dev script fails with "Cannot find module" The dev script runs node --watch dist/index.js. If dist/ does not exist or is stale, run pnpm --filter @familysync/api build first.

DB connection refused when running API on host DB_HOST defaults to mariadb (the Docker service name). When running the API outside Docker, override it: DB_HOST=127.0.0.1. The dev compose override exposes port 3306 on the host.

drizzle-kit migrate says "cannot connect" Same DB_HOST issue. Prepend DB_HOST=127.0.0.1 to the migrate command when running from the host.

API integration tests fail with FK errors The vitest config sets fileParallelism: false to prevent concurrent test files from conflicting via the shared MariaDB. Ensure you are not overriding this. Tests require a running MariaDB — set DB_HOST=127.0.0.1 and ensure the dev database is up.

TypeScript errors missed during development Vite/esbuild strips types; type errors will not surface in vitest run or vite build output. Run pnpm typecheck explicitly to catch them.

pnpm lint warns about ESLint version ESLint is pinned to 9.39.4. Do not upgrade to ESLint 10 until eslint-plugin-react resolves the getFilename is not a function incompatibility (jsx-eslint#3977).