<!-- generated-by: gsd-doc-writer -->

# 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

```bash
pnpm install
```

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

### 2. Start the dev database and Redis

```bash
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`:

```bash
# 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.

```bash
# 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

```bash
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`:

```bash
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

```bash
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:

```bash
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:

```bash
# 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`. <!-- VERIFY: ESLint and Prettier configs are present in apps/api and apps/pwa -->

## Docker Compose Dev Stack

```bash
# 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.
