Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
9.6 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 lint |
Run lint in all workspaces (pnpm -r lint) |
pnpm typecheck |
Run tsc --noEmit in all workspaces |
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 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 build → dist/ |
pnpm --filter @familysync/pwa preview |
Serve the production build locally |
pnpm --filter @familysync/pwa typecheck |
tsc --noEmit |
pnpm --filter @familysync/pwa test |
Run vitest once |
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.
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
Run pnpm typecheck before opening a PR to catch errors that vitest and Vite builds will silently miss.
Code Style
ESLint and Prettier are listed as the intended linting and formatting tools. Check for config files in each workspace and confirm the lint script is wired before running pnpm lint.
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/srcinto 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.