Reformats 4 phase-17 files (SettingsSheet.tsx, tokens.css, vite.config.ts, pwa-assets.config.ts) plus 11 pre-existing non-conformant docs/READMEs that the repo-wide format:check also flags. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
24 KiB
FamilySync — Configuration Reference
All runtime configuration is supplied via environment variables. There are no JSON or YAML config files beyond Docker Compose. Copy .env.example to .env at the repo root and fill in the values before starting any service.
Environment Variables
Database
| Variable | Required | Default | Description |
|---|---|---|---|
DB_HOST |
Yes | localhost |
MariaDB hostname. Use mariadb inside Docker Compose; use localhost (or 127.0.0.1) for host-side dev runs. |
DB_PORT |
No | 3306 |
MariaDB port. |
DB_USER |
No | familysync |
Database user. |
DB_PASSWORD |
Required | (none) | Database password. Also used by the mariadb service as MARIADB_PASSWORD. |
DB_NAME |
No | familysync |
Database name. |
DB_ROOT_PASSWORD |
Required | (none) | MariaDB root password. Used only by the mariadb Docker service (MARIADB_ROOT_PASSWORD). Not read by the API process. |
DB_ROOT_USER |
No | root |
MariaDB root username. Read only by apps/api/test/global-setup.ts during local test provisioning. Never used by the API or Docker Compose in production. |
Five of these variables — DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT — are read by drizzle.config.ts when running migrations (db:generate / db:migrate) and by the API process to build its connection pool. DB_ROOT_PASSWORD is not read by either; it is consumed only by the mariadb Docker service. DB_ROOT_USER is only used by the local Vitest global setup to create and grant the familysync_test database.
Important: Do not use drizzle-kit push against this MariaDB. The mysql dialect mis-reads MariaDB 11.x metadata and schedules false destructive operations. Always use db:generate + db:migrate.
OIDC / Authelia Authentication
| Variable | Required | Default | Description |
|---|---|---|---|
OIDC_AUTH_SECRET |
Required | placeholder_change_me (Docker default) |
32+ character random string used to sign the oidc-auth session JWT cookie. Generate with openssl rand -base64 32. |
OIDC_ISSUER |
Required | (none) | Authelia base URL, e.g. https://auth.DOMAIN. The middleware fetches /.well-known/openid-configuration from this URL. |
OIDC_CLIENT_ID |
No | familysync |
Registered client ID in Authelia. |
OIDC_CLIENT_SECRET |
Required | (none) | Plaintext client secret matching the pbkdf2 hash stored in Authelia's configuration.yml. Do not use the hash here. |
OIDC_REDIRECT_URI |
Required | (none) | Full callback URL registered in Authelia, e.g. https://familysync.DOMAIN/callback. |
OIDC_AUTH_EXTERNAL_URL |
Required | (none) | Public-facing base URL of the app, e.g. https://familysync.DOMAIN. Mandatory behind the Pangolin/Newt tunnel. Without it, @hono/oidc-auth builds the redirect URI from the internal container hostname, which will not match the registered URI and breaks the OIDC flow. |
OIDC_SCOPES |
No | openid profile email offline_access |
Space-separated list of OIDC scopes to request. offline_access is required for refresh-token session persistence. Authelia rejects unknown scopes, so do not add scopes that are not configured on the Authelia client. |
OIDC_AUTH_EXPIRES |
No | 86400 |
Session cookie Max-Age in seconds (default 1 day). Governs the persistent session cookie set by persistSessionCookie middleware. |
OIDC_COOKIE_NAME |
No | oidc-auth |
Name of the session cookie. Override only if you need to run multiple instances under the same domain. |
OIDC_COOKIE_PATH |
No | / |
Cookie Path attribute. |
OIDC_COOKIE_DOMAIN |
No | (not set) | Cookie Domain attribute. Set this when the API and PWA are served from different subdomains under the same apex domain. Must match the Authelia and app domains (same-parent-domain requirement). |
Locked Authelia client parameters (these are fixed by the project; do not change):
response_types: [code]grant_types: [authorization_code, refresh_token]require_pkce: true,pkce_challenge_method: S256token_endpoint_auth_method: client_secret_basic
Local Authentication (No-OIDC Mode)
These variables govern the stateless local-auth path introduced in Phase 19. Local auth issues a separate local-session JWT cookie (distinct from oidc-auth) signed with LOCAL_SESSION_SECRET.
| Variable | Required | Default | Description |
|---|---|---|---|
LOCAL_SESSION_SECRET |
Required (non-bypass) | (none) | 32+ character secret used to sign and verify local-session JWT cookies (HS256). Generate with openssl rand -base64 32. The API refuses to start with a fatal error if this is absent or shorter than 32 characters, unless DEV_AUTH_BYPASS=true. |
LOCAL_SESSION_EXPIRES |
No | 86400 |
local-session cookie Max-Age in seconds (default 1 day). Mirrors OIDC_AUTH_EXPIRES but applies to the local-auth cookie. Malformed (non-numeric) values silently fall back to the default. Source: apps/api/src/auth/localSession.ts. |
Security note: LOCAL_SESSION_SECRET must be a distinct value from OIDC_AUTH_SECRET. Both are JWT signing keys, but they govern different cookies and must not be shared.
Broker Encryption
| Variable | Required | Default | Description |
|---|---|---|---|
APP_PASSWORD_ENCRYPTION_KEY |
Required | (none) | 64-character hex string (32 bytes) used as the AES-256-GCM key for encrypting Fastmail app passwords at rest. Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))". Startup throws if this is missing or the wrong length. |
Web Push (VAPID)
| Variable | Required | Default | Description |
|---|---|---|---|
VAPID_PUBLIC_KEY |
No* | (none) | URL-safe base64 VAPID public key. Served to the PWA via GET /api/push/vapid-public-key. Non-secret. |
VAPID_PRIVATE_KEY |
No* | (none) | URL-safe base64 VAPID private key. Used server-side to sign push requests. Never expose to clients. |
VAPID_SUBJECT |
No* | (none) | Contact URI identifying the operator, e.g. mailto:admin@example.com or https://familysync.DOMAIN. Must be a mailto: or https: URL. |
*All three VAPID variables are optional in the sense that the server starts without them, but push notifications will not function. A startup warning is logged if any are missing. Generate a key pair with:
npx web-push generate-vapid-keys --json
Runtime Mode
| Variable | Required | Default | Description |
|---|---|---|---|
NODE_ENV |
No | (not set) | Set to production in the production Docker Compose. When production, the dev-auth bypass is unconditionally disabled regardless of DEV_AUTH_BYPASS. |
DEV_AUTH_BYPASS |
No | (not set) | Set to true to bypass OIDC authentication for local development without a live Authelia instance. Only active when NODE_ENV !== 'production'. The production docker-compose.yml must never include this variable. |
TZ |
No | (not set) | IANA timezone identifier (e.g. America/Toronto) used as the server-side fallback for the household timezone when no value is stored in app_config. The full fallback chain is: stored DB value → TZ env → Intl.DateTimeFormat().resolvedOptions().timeZone. Empty or whitespace values are ignored. Source: apps/api/src/lib/householdTimezone.ts. |
Developer / Test-Only Variables
These variables are never needed in production and should not appear in the production .env.
| Variable | Scope | Default | Description |
|---|---|---|---|
FASTMAIL_EMAIL |
Dev spike script only | (none) | Fastmail account email. Read only by apps/api/src/broker/spike.ts, a standalone dev script for enumerating CalDAV collections. Not imported by the API or Docker image. |
FASTMAIL_APP_PASSWORD |
Dev spike script only | (none) | Fastmail app password. Read only by apps/api/src/broker/spike.ts. Never logged. Not used by the API in any environment. |
PLAYWRIGHT_BASE_URL |
E2E tests only | http://localhost:5173 |
Base URL for Playwright e2e tests. Overridden to http://127.0.0.1:5173 in CI to avoid IPv6 resolution failures. Source: apps/pwa/playwright.config.ts. |
Defaults Summary
Variables with non-empty defaults do not cause startup failure if absent, but should be reviewed for production:
| Variable | Default | Source |
|---|---|---|
DB_HOST |
localhost |
apps/api/src/db/client.ts |
DB_PORT |
3306 |
apps/api/src/db/client.ts |
DB_USER |
familysync |
apps/api/src/db/client.ts |
DB_NAME |
familysync |
apps/api/src/db/client.ts |
OIDC_CLIENT_ID |
familysync |
docker-compose.yml |
OIDC_SCOPES |
openid profile email offline_access |
docker-compose.yml |
OIDC_AUTH_EXPIRES |
86400 |
apps/api/src/auth/persistSessionCookie.ts |
OIDC_COOKIE_NAME |
oidc-auth |
apps/api/src/auth/persistSessionCookie.ts |
OIDC_COOKIE_PATH |
/ |
apps/api/src/auth/persistSessionCookie.ts |
LOCAL_SESSION_EXPIRES |
86400 |
apps/api/src/auth/localSession.ts |
Per-Environment Configuration
Production (Docker Compose)
docker-compose.yml sets NODE_ENV=production and injects all secrets via ${VAR} interpolation from a .env file on the Docker host. No .env file is committed to the repository.
Minimum production .env:
# Database
DB_PASSWORD=<strong-password>
DB_ROOT_PASSWORD=<strong-root-password>
# OIDC
OIDC_AUTH_SECRET=<openssl rand -base64 32>
OIDC_ISSUER=https://auth.DOMAIN
OIDC_CLIENT_SECRET=<plaintext secret>
OIDC_REDIRECT_URI=https://familysync.DOMAIN/callback
OIDC_AUTH_EXTERNAL_URL=https://familysync.DOMAIN
# Local auth (Phase 19)
LOCAL_SESSION_SECRET=<openssl rand -base64 32>
# Broker encryption
APP_PASSWORD_ENCRYPTION_KEY=<64-hex-chars>
# VAPID push (optional but recommended)
VAPID_PUBLIC_KEY=<base64>
VAPID_PRIVATE_KEY=<base64>
VAPID_SUBJECT=mailto:admin@example.com
Local Development (host-side)
The dev Docker Compose override (docker-compose.dev.yml) exposes MariaDB on localhost:3306 and Redis on localhost:6379. To run the API and PWA directly on the host:
# Build the API first (dev script runs compiled output)
pnpm --filter @familysync/api build
# Source .env, then override DB_HOST and activate the bypass
set -a; source .env; set +a && DEV_AUTH_BYPASS=true DB_HOST=localhost pnpm --filter @familysync/api dev
# PWA (separate terminal)
pnpm --filter @familysync/pwa dev
The DB_HOST override is necessary because .env sets DB_HOST=mariadb (the Docker network hostname), which does not resolve on the host. The set -a; source .env; set +a idiom exports all variables; the DB_HOST=localhost prefix on the same line overrides that one variable for the child process.
DEV_AUTH_BYPASS=true is a local-only option. The API hard-checks NODE_ENV === 'production' before reading DEV_AUTH_BYPASS — the bypass has zero effect in a production container even if the variable is present.
The dev Docker Compose (docker-compose.dev.yml) sets LOCAL_SESSION_SECRET to a fixed dev placeholder value (dev-secret-change-me-0000000000000000). This value is intentionally weak and public — it is never used in production.
Test
Integration tests targeting the real database require the dev MariaDB running with the host port exposed and the following overrides:
DB_HOST=127.0.0.1 DB_PORT=3306 DB_USER=familysync DB_NAME=familysync DB_PASSWORD=<value>
The Vitest global setup (apps/api/test/global-setup.ts) also reads DB_ROOT_USER (default root) and DB_ROOT_PASSWORD (default root) to create and grant the familysync_test database on first run. These are local dev credentials only; CI uses hardcoded throwaway values (familysync / testpass) in ephemeral service containers.
See docs/deployment.md for the full drizzle-kit migrate command used to prepare the test database.
Config File Reference
Application Config Files
There are no application-level JSON/YAML config files. The two config files that read environment variables at dev/build time are:
| File | Purpose |
|---|---|
apps/api/drizzle.config.ts |
Drizzle Kit migration config — reads DB_* variables |
apps/pwa/vite.config.ts |
Vite build config — no env var reads; proxy rules for dev server |
The PWA Vite dev server proxies /health, /api, and /callback to http://localhost:3000 so the frontend and API can be developed without CORS configuration.
Lint and Format Config Files
These files live at the repo root and apply to both apps/api and apps/pwa.
eslint.config.js
ESLint 9 flat config (ESM). Pinned to ESLint 9.39.4 — do not upgrade to ESLint 10 until eslint-plugin-react resolves the getFilename is not a function incompatibility.
Key layers (in order):
- Global ignores —
**/dist/**,**/node_modules/**,apps/api/src/db/migrations/**,pnpm-lock.yaml. - Base TS/TSX (
apps/**/*.{ts,tsx}) —js.configs.recommended+tseslint.configs.recommendedTypeCheckedwithprojectService: true(auto-discovers alltsconfig.jsonfiles).@typescript-eslint/no-unused-varsallows_-prefixed names. - React + Hooks (
apps/pwa/**/*.{ts,tsx}only) —eslint-plugin-reactflat recommended +eslint-plugin-react-hooksflat recommended. React Compiler rules (immutability, purity, refs, etc.) are disabled — this codebase does not use the React Compiler. disableTypeCheckedoverride — applied to tool config files and test/e2e directories that are outside anytsconfigproject (apps/api/drizzle.config.ts,apps/api/vitest.config.ts,apps/pwa/vite.config.ts,apps/pwa/vitest.config.ts,apps/pwa/playwright.config.ts,apps/api/tests/**/*.ts,apps/pwa/e2e/**/*.ts,eslint.config.js). Type-aware rules are disabled for these files; non-type-aware rules still apply.eslint-config-prettier(last) — disables all ESLint formatting rules that conflict with Prettier.
Run lint: pnpm lint (delegates to pnpm -r --if-present lint across all workspaces).
.prettierrc
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100
}
Run formatter: pnpm format (write) or pnpm format:check (CI check, no writes).
.prettierignore
Excludes dist/, node_modules/, .pnpm-store/, pnpm-lock.yaml, apps/api/src/db/migrations/, *.html, and .planning/ from formatting. The .pnpm-store/ exclusion covers CI runners that have no persistent global pnpm store and land the content-addressable store inside the workspace.
CI Secrets
The Gitea Actions workflows in .gitea/workflows/ require one repository secret.
| Secret | Scope | Description |
|---|---|---|
REGISTRY_PAT |
publish.yml only |
A Gitea user PAT with write:package scope. Used to authenticate docker login against the Gitea container registry before pushing the API image. Must be named REGISTRY_PAT — Gitea reserves the GITEA_ prefix for built-in variables, so any GITEA_-prefixed secret name is rejected. GITEA_TOKEN and GITHUB_TOKEN do not have package-push permissions. |
CI Database Credentials
The ci.yml api and harness jobs spin up a throwaway MariaDB service container with hardcoded credentials (familysync / testpass). These are ephemeral — scoped to a single job container — and are not production secrets. Do not reuse them outside CI.