docs: add contributing guide, env reference, and operations runbook
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
# Contributing
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js** 22 LTS (`node --version` should print `v22.x.x`)
|
||||
- **pnpm** 10 (`pnpm --version` should print `10.x.x`)
|
||||
- **Docker** with the Compose plugin (used for the local Postgres + MinIO containers)
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ginnoir/famapp.git
|
||||
cd famapp
|
||||
pnpm install
|
||||
cp .env.example .env
|
||||
# Edit .env — minimum required for local dev:
|
||||
# NEXT_PUBLIC_APP_URL=http://127.0.0.1:3000
|
||||
# DATABASE_URL=postgres://famapp:famapp@localhost:5432/famapp
|
||||
# ENABLE_DEV_LOGIN=true
|
||||
# AUTH_SECRET=<any-32-char-string-for-local>
|
||||
```
|
||||
|
||||
Start everything (database + migrations + seed + dev server):
|
||||
|
||||
```bash
|
||||
pnpm dev:local
|
||||
```
|
||||
|
||||
Then open `http://localhost:3000/login` and click **Dev login**. See `docs/dev-login.md` for the full local setup including HTTPS/PWA testing.
|
||||
|
||||
## Available scripts
|
||||
|
||||
<!-- AUTO-GENERATED from package.json scripts -->
|
||||
|
||||
| Command | Description |
|
||||
| --------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `pnpm dev` | Next.js dev server (localhost only) |
|
||||
| `pnpm dev:network` | Dev server bound to `0.0.0.0` (LAN access for phone testing) |
|
||||
| `pnpm dev:local` | Full local stack — starts DB container, runs migrations, seeds, launches dev server |
|
||||
| `pnpm dev:reset` | Tear down local DB and start fresh (destructive — deletes all local data) |
|
||||
| `pnpm build` | Production Next.js build |
|
||||
| `pnpm start` | Start the production build locally |
|
||||
| `pnpm lint` | ESLint check |
|
||||
| `pnpm lint:fix` | ESLint with auto-fix |
|
||||
| `pnpm format` | Prettier — format all files |
|
||||
| `pnpm format:check` | Prettier — check only (used in CI) |
|
||||
| `pnpm typecheck` | TypeScript type check (`tsc --noEmit`) |
|
||||
| `pnpm test:e2e` | Playwright end-to-end tests |
|
||||
| `pnpm db:generate` | Generate a new Drizzle migration from schema changes |
|
||||
| `pnpm db:migrate` | Apply pending Drizzle migrations |
|
||||
| `pnpm db:seed` | Seed the database with dev fixtures |
|
||||
| `pnpm db:studio` | Open Drizzle Studio (local DB browser) |
|
||||
| `pnpm gen:icons` | Regenerate PWA icon set from source |
|
||||
| `pnpm vapid:generate` | Generate VAPID key pair for Web Push |
|
||||
| `pnpm release` | Interactive release (prompts for semver bump, tags, publishes changelog + GitHub Release) |
|
||||
| `pnpm release:patch` | Non-interactive patch release |
|
||||
| `pnpm release:minor` | Non-interactive minor release |
|
||||
| `pnpm release:major` | Non-interactive major release |
|
||||
| `pnpm release:dry` | Dry-run release — preview without writing |
|
||||
|
||||
<!-- END AUTO-GENERATED -->
|
||||
|
||||
## Running tests
|
||||
|
||||
### Type check + lint (CI equivalent)
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
pnpm lint
|
||||
pnpm format:check
|
||||
```
|
||||
|
||||
### End-to-end tests
|
||||
|
||||
The app must be running first (`pnpm dev:local`). Generate a Playwright auth state file, then run tests:
|
||||
|
||||
```powershell
|
||||
# Generate auth state (run once after starting the app)
|
||||
New-Item -ItemType Directory -Force tests\.auth | Out-Null
|
||||
@'
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.goto('http://127.0.0.1:3000/login');
|
||||
await page.getByRole('button', { name: 'Dev login' }).click();
|
||||
await page.waitForURL('http://127.0.0.1:3000/');
|
||||
await page.context().storageState({ path: 'tests/.auth/dev-user.json' });
|
||||
await browser.close();
|
||||
})();
|
||||
'@ | node -
|
||||
|
||||
# Run tests
|
||||
$env:PLAYWRIGHT_STORAGE_STATE='tests/.auth/dev-user.json'
|
||||
pnpm test:e2e
|
||||
```
|
||||
|
||||
### Writing tests
|
||||
|
||||
- Unit tests: Vitest under `tests/unit/` — only where it pays off (utilities, pure logic).
|
||||
- E2E tests: Playwright under `tests/e2e/` — one happy-path test per module. Do not write brittle selector-heavy tests for trivial CRUD.
|
||||
|
||||
## Code style
|
||||
|
||||
- **TypeScript strict** — no `any` without a written reason.
|
||||
- **No comments** unless the _why_ is non-obvious. Names carry intent.
|
||||
- **Immutable** — always return new objects; never mutate in place.
|
||||
- **Module isolation** — a module imports from `_core` and `lib/` only; never from a sibling module.
|
||||
- **File size** — 200–400 lines typical, 800 hard cap.
|
||||
|
||||
Formatting and lint run automatically on staged files via `lint-staged` at commit time. You can also run them manually with `pnpm lint:fix` and `pnpm format`.
|
||||
|
||||
## Commit format
|
||||
|
||||
Commits must follow [Conventional Commits](https://www.conventionalcommits.org/). `commitlint` enforces this at the `commit-msg` hook.
|
||||
|
||||
```
|
||||
<type>: <short description>
|
||||
|
||||
[optional body]
|
||||
```
|
||||
|
||||
Allowed types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `ci`, `revert`
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
feat: add plant species search to garden module
|
||||
fix: calendar event end time off by one day
|
||||
chore: bump next to 15.6
|
||||
```
|
||||
|
||||
## Adding a module
|
||||
|
||||
Every feature lives under `src/modules/<name>/`. A new module needs:
|
||||
|
||||
- `schema.ts` — Drizzle tables
|
||||
- `server/` — server actions and queries
|
||||
- `components/` — React components
|
||||
- `manifest.ts` — registers the module with the core registry (nav, entity types, dashboard widget, quick-add actions)
|
||||
|
||||
See `CLAUDE.md` for the full architectural brief. The module loader in `src/modules/_core/` discovers manifests automatically — no changes to core code required for a new module.
|
||||
|
||||
## Schema changes
|
||||
|
||||
1. Edit the relevant `schema.ts`.
|
||||
2. Run `pnpm db:generate` to create a new migration file under `drizzle/`.
|
||||
3. Commit the migration alongside the schema change.
|
||||
4. Never edit a shipped migration — always add a new one.
|
||||
|
||||
## PR checklist
|
||||
|
||||
- [ ] `pnpm typecheck` passes
|
||||
- [ ] `pnpm lint` passes
|
||||
- [ ] `pnpm build` succeeds
|
||||
- [ ] New Drizzle migration committed if schema changed
|
||||
- [ ] E2E test added or updated if a user-visible flow changed
|
||||
- [ ] `docs/tasks/09-pre-deploy-checklist.md` reviewed if touching auth or env
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
# Environment Variables
|
||||
|
||||
<!-- AUTO-GENERATED from .env.example and deploy/compose.example.yaml -->
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Fill in required values, then:
|
||||
pnpm dev:local
|
||||
```
|
||||
|
||||
## Core
|
||||
|
||||
| Variable | Required | Description | Example / Default |
|
||||
| --------------------- | -------- | ------------------------------------------------------------------ | ------------------------------------------------ |
|
||||
| `NEXT_PUBLIC_APP_URL` | Yes | Public URL of the app — used in OIDC redirect URIs and share links | `https://fam.yourdomain.com` |
|
||||
| `DATABASE_URL` | Yes | Postgres connection string | `postgres://famapp:famapp@localhost:5432/famapp` |
|
||||
| `AUTH_SECRET` | Yes | Auth.js signing secret — generate with `openssl rand -base64 32` | — |
|
||||
|
||||
## OIDC (Authentik)
|
||||
|
||||
Required in production. Obtain from the Authentik admin panel after bootstrapping the provider — see `deploy/authentik/README.md`.
|
||||
|
||||
| Variable | Required | Description | Example |
|
||||
| ------------------------- | ---------- | --------------------------------- | --------------------------------------------------- |
|
||||
| `AUTH_OIDC_ISSUER` | Yes (prod) | Authentik OIDC issuer URL | `https://auth.yourdomain.com/application/o/famapp/` |
|
||||
| `AUTH_OIDC_CLIENT_ID` | Yes (prod) | OIDC client ID from Authentik | — |
|
||||
| `AUTH_OIDC_CLIENT_SECRET` | Yes (prod) | OIDC client secret from Authentik | — |
|
||||
|
||||
## Dev login
|
||||
|
||||
Local development only. Never set `ENABLE_DEV_LOGIN=true` in production — a startup assertion in `src/lib/dev-login-config.ts` will crash the container if you do.
|
||||
|
||||
| Variable | Required | Description | Default |
|
||||
| -------------------- | -------- | ------------------------------------------------- | ------------------ |
|
||||
| `ENABLE_DEV_LOGIN` | No | Show a one-click **Dev login** button on `/login` | `false` |
|
||||
| `DEV_LOGIN_EMAIL` | No | Email for the auto-created dev session | `dev@famapp.local` |
|
||||
| `DEV_LOGIN_NAME` | No | Display name for the dev user | `Dev User` |
|
||||
| `DEV_HOUSEHOLD_NAME` | No | Household created for the dev user | `Home` |
|
||||
|
||||
See `docs/dev-login.md` for the full local run procedure.
|
||||
|
||||
## Web Push (VAPID)
|
||||
|
||||
All three are required together. Generate them once with `pnpm vapid:generate` and copy all three lines into `.env`.
|
||||
|
||||
| Variable | Required | Description |
|
||||
| ------------------- | -------- | --------------------------------------------- | ------------------------ |
|
||||
| `VAPID_PUBLIC_KEY` | Yes | VAPID public key — also passed to the browser |
|
||||
| `VAPID_PRIVATE_KEY` | Yes | VAPID private key — server only |
|
||||
| `VAPID_SUBJECT` | Yes | Contact URI for push servers | `mailto:you@example.com` |
|
||||
|
||||
## ntfy (optional)
|
||||
|
||||
Leave both blank to disable the ntfy notification channel. Web Push is the primary channel.
|
||||
|
||||
| Variable | Required | Description | Example |
|
||||
| ------------ | -------- | ------------------------ | ----------------- |
|
||||
| `NTFY_URL` | No | ntfy server base URL | `https://ntfy.sh` |
|
||||
| `NTFY_TOPIC` | No | ntfy topic to publish to | `famapp-alerts` |
|
||||
|
||||
## Logging
|
||||
|
||||
| Variable | Required | Description | Values |
|
||||
| ----------- | -------- | ------------------ | ----------------------------------------------------------- |
|
||||
| `LOG_LEVEL` | No | Pino log verbosity | `trace`, `debug`, `info`, `warn`, `error` (default: `info`) |
|
||||
|
||||
## MinIO object storage
|
||||
|
||||
Required when the garden module is enabled. The dev compose stack starts a local MinIO instance automatically via `pnpm dev:local`.
|
||||
|
||||
| Variable | Required | Description | Default |
|
||||
| --------------------- | -------- | ------------------------------------ | ----------------------- |
|
||||
| `MINIO_ENDPOINT` | Yes | MinIO server base URL | `http://localhost:9000` |
|
||||
| `MINIO_ROOT_USER` | Yes | MinIO root access key | `famapp` |
|
||||
| `MINIO_ROOT_PASSWORD` | Yes | MinIO root secret key | `changeme` |
|
||||
| `MINIO_BUCKET` | No | Bucket used for garden image uploads | `garden` |
|
||||
|
||||
## OpenPlantBook
|
||||
|
||||
Optional. Enables plant species lookup in the garden module. Free account at <https://open.plantbook.io>.
|
||||
|
||||
| Variable | Required | Description |
|
||||
| ----------------------------- | -------- | -------------------- |
|
||||
| `OPENPLANTBOOK_CLIENT_ID` | No | OAuth2 client ID |
|
||||
| `OPENPLANTBOOK_CLIENT_SECRET` | No | OAuth2 client secret |
|
||||
|
||||
## Release tooling
|
||||
|
||||
Only needed on the machine that cuts releases (`pnpm release`).
|
||||
|
||||
| Variable | Required | Description |
|
||||
| -------------- | ------------- | ------------------------------------------------ |
|
||||
| `GITHUB_TOKEN` | Yes (release) | Personal access token — creates a GitHub Release |
|
||||
|
||||
## Production-only compose variables
|
||||
|
||||
Used by `deploy/compose.example.yaml`. Set in `deploy/.env` on the server — not in the local `.env`.
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `FAMAPP_IMAGE` | Docker image tag to deploy (e.g. `ghcr.io/ginnoir/famapp:v0.4.7`) |
|
||||
| `FAMAPP_PORT` | Host port to bind (default: `3000`) |
|
||||
| `FAMAPP_PULL_POLICY` | Docker pull policy (default: `always`) |
|
||||
| `FAMAPP_DB_USER` / `FAMAPP_DB_PASSWORD` / `FAMAPP_DB_NAME` | Postgres credentials for the famapp database |
|
||||
| `AUTHENTIK_DB_USER` / `AUTHENTIK_DB_PASSWORD` / `AUTHENTIK_DB_NAME` | Postgres credentials for the Authentik database |
|
||||
| `AUTHENTIK_SECRET_KEY` | Authentik signing key — generate with `openssl rand -base64 60` |
|
||||
| `AUTHENTIK_IMAGE_TAG` | Authentik server image tag (default: `2024.12.3`) |
|
||||
| `RUN_MIGRATIONS` | Set `false` to skip auto-migration on start (default: `true`) |
|
||||
|
||||
<!-- END AUTO-GENERATED -->
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
# Runbook
|
||||
|
||||
Operational reference for famapp in production. See `deploy/README.md` for the one-time host setup and `docs/tasks/09-pre-deploy-checklist.md` for the pre-deploy checklist.
|
||||
|
||||
## Cutting a release
|
||||
|
||||
From `main` on the dev machine, with a clean working tree:
|
||||
|
||||
```bash
|
||||
pnpm release:patch # or :minor / :major
|
||||
# — bumps package.json version
|
||||
# — prepends to CHANGELOG.md
|
||||
# — creates and pushes a signed git tag (v0.x.y)
|
||||
# — creates a GitHub Release with generated notes
|
||||
# Requires GITHUB_TOKEN in .env
|
||||
```
|
||||
|
||||
CI (`release.yml`) then builds and pushes the Docker image to GHCR:
|
||||
|
||||
- `ghcr.io/ginnoir/famapp:v0.x.y`
|
||||
- `ghcr.io/ginnoir/famapp:0.x` (minor alias)
|
||||
- `ghcr.io/ginnoir/famapp:latest`
|
||||
|
||||
## Deploying a release
|
||||
|
||||
On the home server, in `/srv/famapp/deploy/`:
|
||||
|
||||
```bash
|
||||
# Pin the new tag
|
||||
sed -i 's|FAMAPP_IMAGE=.*|FAMAPP_IMAGE=ghcr.io/ginnoir/famapp:v0.x.y|' .env
|
||||
|
||||
# Pull and restart only the app container
|
||||
docker compose pull famapp
|
||||
docker compose up -d famapp
|
||||
|
||||
# Watch the boot log — migrations run before the server starts
|
||||
docker compose logs -f famapp
|
||||
```
|
||||
|
||||
The container entrypoint runs `node scripts/migrate.mjs` automatically. A healthy boot ends with a log line like `ready on http://0.0.0.0:3000`.
|
||||
|
||||
## Health check
|
||||
|
||||
```bash
|
||||
# Container status
|
||||
docker compose ps
|
||||
|
||||
# App response (200 = healthy)
|
||||
curl -sf https://fam.yourdomain.com/ -o /dev/null -w "%{http_code}\n"
|
||||
|
||||
# Recent app logs
|
||||
docker compose logs --tail=100 famapp
|
||||
|
||||
# Database connectivity
|
||||
docker compose exec famapp-db pg_isready -U famapp -d famapp
|
||||
```
|
||||
|
||||
## Rollback
|
||||
|
||||
1. Find the previous working tag in `CHANGELOG.md` or `docker images`.
|
||||
2. Pin it in `deploy/.env`:
|
||||
```bash
|
||||
sed -i 's|FAMAPP_IMAGE=.*|FAMAPP_IMAGE=ghcr.io/ginnoir/famapp:v0.x.y|' .env
|
||||
```
|
||||
3. Restart the container:
|
||||
```bash
|
||||
docker compose up -d famapp
|
||||
```
|
||||
|
||||
If the rollback target predates a migration that has already been applied to the database, restore from backup first — see **Backups** below. To skip auto-migration on a given start (rarely needed):
|
||||
|
||||
```bash
|
||||
RUN_MIGRATIONS=false docker compose up -d famapp
|
||||
```
|
||||
|
||||
## Backups
|
||||
|
||||
The `famapp-backup` container runs nightly `pg_dump` for both `famapp` and `authentik` databases into the `backups` volume.
|
||||
|
||||
```bash
|
||||
# Check last backup run
|
||||
docker compose logs famapp-backup | tail -20
|
||||
|
||||
# List backup files
|
||||
docker compose exec famapp-backup ls -lh /backups
|
||||
|
||||
# Manual backup now
|
||||
docker compose exec famapp-backup sh /scripts/backup.sh
|
||||
|
||||
# Restore from a backup file
|
||||
docker compose exec famapp-backup sh /scripts/restore.sh famapp_2026-06-01.sql.gz
|
||||
```
|
||||
|
||||
See `deploy/backups/README.md` for retention policy and full restore details.
|
||||
|
||||
## Common issues
|
||||
|
||||
### App container exits immediately
|
||||
|
||||
```bash
|
||||
docker compose logs famapp
|
||||
```
|
||||
|
||||
Likely causes:
|
||||
|
||||
- **Missing required env var** — the app throws on startup if `AUTH_SECRET`, `DATABASE_URL`, or `AUTH_OIDC_*` are absent.
|
||||
- **`ENABLE_DEV_LOGIN=true` in production** — `src/lib/dev-login-config.ts` throws an assertion error on import. Remove the variable from the production env.
|
||||
- **Database not ready** — Postgres healthcheck should prevent this, but if the DB is slow to start, increase `start_period` in `compose.yaml`.
|
||||
|
||||
### Migrations fail on boot
|
||||
|
||||
```bash
|
||||
docker compose logs famapp | grep -i migration
|
||||
```
|
||||
|
||||
- Schema is ahead of code: roll back the image or forward-migrate manually.
|
||||
- Database unreachable: confirm `famapp-db` is healthy (`docker compose ps`).
|
||||
|
||||
### OIDC login fails
|
||||
|
||||
1. Confirm `AUTH_OIDC_ISSUER` matches the Authentik provider URL exactly (trailing slash matters).
|
||||
2. Confirm the OIDC client redirect URI in Authentik includes `https://fam.yourdomain.com/api/auth/callback/oidc`.
|
||||
3. Check Authentik logs: `docker compose logs authentik-server | tail -50`.
|
||||
|
||||
### Push notifications not arriving
|
||||
|
||||
1. Confirm `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY`, and `VAPID_SUBJECT` are all set in the production env.
|
||||
2. Verify the browser's push subscription is still valid (Settings → notifications → re-enable).
|
||||
3. If using ntfy as a secondary channel, confirm `NTFY_URL` and `NTFY_TOPIC` are set.
|
||||
|
||||
### MinIO / garden image uploads failing
|
||||
|
||||
```bash
|
||||
docker compose logs famapp-minio | tail -30
|
||||
```
|
||||
|
||||
- Confirm `MINIO_ENDPOINT` is `http://famapp-minio:9000` in the compose env (not `localhost`).
|
||||
- Confirm the `garden` bucket exists — create it manually via the MinIO console at port `9001` if it is missing.
|
||||
|
||||
### Out of disk space
|
||||
|
||||
```bash
|
||||
df -h /var/lib/docker
|
||||
docker system prune --volumes # removes stopped containers, dangling images, unused volumes
|
||||
```
|
||||
|
||||
Old backup files accumulate in the `backups` volume. The retention script (`deploy/backups/retain.sh`) runs nightly — check its logs if the volume keeps growing.
|
||||
|
||||
## Authentik admin access
|
||||
|
||||
The Authentik admin UI is at `https://auth.yourdomain.com`. Log in with the superuser credentials set during bootstrap — see `deploy/authentik/README.md`.
|
||||
|
||||
To reset the Authentik admin password from the CLI:
|
||||
|
||||
```bash
|
||||
docker compose exec authentik-server ak create_recovery_key 1 akadmin
|
||||
# prints a one-time recovery URL
|
||||
```
|
||||
Reference in New Issue
Block a user