Files
familysync/docs/CONFIGURATION.md
T
2026-06-18 21:06:32 -04:00

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: S256
  • token_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. 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):

  1. Global ignores**/dist/**, **/node_modules/**, apps/api/src/db/migrations/**, pnpm-lock.yaml.
  2. Base TS/TSX (apps/**/*.{ts,tsx}) — js.configs.recommended + tseslint.configs.recommendedTypeChecked with projectService: true (auto-discovers all tsconfig.json files). @typescript-eslint/no-unused-vars allows _-prefixed names.
  3. React + Hooks (apps/pwa/**/*.{ts,tsx} only) — eslint-plugin-react flat recommended + eslint-plugin-react-hooks flat recommended. React Compiler rules (immutability, purity, refs, etc.) are disabled — this codebase does not use the React Compiler.
  4. disableTypeChecked override — applied to tool config files and test/e2e directories that are outside any tsconfig project (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.
  5. 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.