Author SHA1 Message Date
Lucas Berger 96bb3144ec docs(quick-260618-smr): remove unused Redis service and references
CI / changes (pull_request) Successful in 4s
CI / api (pull_request) Successful in 2m3s
CI / fast-checks (pull_request) Successful in 2m28s
CI / security (pull_request) Successful in 57s
CI / harness (pull_request) Successful in 5m25s
CI / gate (pull_request) Successful in 3s
2026-06-18 20:44:32 -04:00
Lucas Berger 0b4266628b docs(260618-smr): remove Redis references from documentation and e2e config
- CLAUDE.md: remove Redis constraint, ioredis library row, Redis architecture sentence, update compose comment
- README.md: remove Redis from prereqs, quick-start command, compose description, tech-stack live-sync row
- docs/ARCHITECTURE.md: delete Redis infrastructure table row
- docs/CONFIGURATION.md: remove Redis localhost:6379 mention from dev section
- docs/deployment.md: delete redis services table row
- docs/DEVELOPMENT.md: remove prereq mention, heading, two up commands, expose bullet, stack comment
- docs/GETTING-STARTED.md: prereq row, up command, prose mention
- docs/TESTING.md: 'API and MariaDB' (was API, MariaDB, and Redis)
- apps/pwa/e2e/README.md: delete Redis on :6379 bullet
- apps/pwa/playwright.config.ts: two comments updated (API+MariaDB, not API+MariaDB+Redis)
2026-06-18 20:43:12 -04:00
Lucas Berger b968f5d53b chore(260618-smr): remove redis service from compose files
- docker-compose.yml: delete redis service block (redis:7-alpine)
- docker-compose.dev.yml: delete redis ports override block (6379:6379)
2026-06-18 20:43:12 -04:00
15 changed files with 173 additions and 38 deletions
+1
View File
@@ -256,6 +256,7 @@ Recent decisions affecting current work:
| 260613-dmw | Exclude `.gitea/**` from the CI `changes` `code` paths-filter so workflow-only PRs skip the heavy api/harness jobs (treated like docs) while fast-checks + gate still run. Single `- '!.gitea/**'` negation appended after the yml/yaml globs (index 11 vs 5). Rides along on the Phase 16 branch / PR #15. | 2026-06-13 | 2d329a9 | | [260613-dmw-exclude-gitea-workflow-config-changes-fr](./quick/260613-dmw-exclude-gitea-workflow-config-changes-fr/) |
| 260613-fp9 | `.gitea`/`.planning`-only pushes to main no longer trigger the Docker publish — added `paths-ignore: ['.gitea/**', '.planning/**']` under `on.push` in `.gitea/workflows/publish.yml` (skips only when EVERY changed file matches; mixed code+docs pushes still publish). `.dockerignore` already excludes `.planning` so the image is byte-identical. Done in isolated worktree (phase-10 agent held main tree). | 2026-06-13 | cd5a88c | | [260613-fp9-gitea-and-planning-pushes-should-not-tri](./quick/260613-fp9-gitea-and-planning-pushes-should-not-tri/) |
| 260613-ndv | Isolate local apps/api integration tests to a dedicated `familysync_test` DB so test runs stop polluting the dev `familysync` DB. New CI-gated vitest globalSetup root-provisions (CREATE DATABASE + GRANT) + migrates + truncate-resets `familysync_test` each run; `vitest.config.ts` forces `DB_NAME=familysync_test` for local workers (no-op under CI, so CI's `familysync` service DB + db:migrate are untouched). Verified: dev `familysync` users stays 3 across a run, `familysync_test` resets (186→93, not doubled), 244/244 tests pass (flaky list_shares timeout gone), typecheck 0. Branch off main. | 2026-06-13 | 07d5161 | Verified | [260613-ndv-wire-apps-api-integration-tests-to-a-ded](./quick/260613-ndv-wire-apps-api-integration-tests-to-a-ded/) |
| 260618-smr | Remove unused Redis service and all references — Redis confirmed unused at runtime (no ioredis/redis client import, no `REDIS_*` env, not a dependency in any package.json). Dropped the `redis` service from both compose files and cleaned all references in CLAUDE.md, README.md, and docs/* + e2e config. Kept the in-memory-vs-Redis design-rationale comments (D-12/D-18) in listEmitter/reminderScheduler/linkNonceStore/localAuth. `docker compose config` parses clean (0 redis); `format:check` green. Branch off main. | 2026-06-18 | 0b42666 | Verified | [260618-smr-remove-unused-redis-service-and-referenc](./quick/260618-smr-remove-unused-redis-service-and-referenc/) |
## Deferred Items
@@ -0,0 +1,83 @@
---
quick_id: 260618-smr
slug: remove-unused-redis-service-and-referenc
description: Remove unused Redis service and references
type: quick
created: 2026-06-19
files_modified:
- docker-compose.yml
- docker-compose.dev.yml
- CLAUDE.md
- README.md
- docs/ARCHITECTURE.md
- docs/CONFIGURATION.md
- docs/deployment.md
- docs/DEVELOPMENT.md
- docs/GETTING-STARTED.md
- docs/TESTING.md
- apps/pwa/e2e/README.md
- apps/pwa/playwright.config.ts
---
# Quick Task 260618-smr: Remove unused Redis service and references
## Why
Redis is confirmed **unused at runtime**: no `ioredis`/redis client import, no `REDIS_*`
env vars read in code, and `ioredis` is not a dependency in any `package.json`. It exists
only as a compose service + documentation references that imply it is part of the stack or
"reserved for future pub/sub". Decision: drop the container and all references (keep the
in-memory-vs-Redis design-rationale comments — see Constraint below).
## Tasks
### Task 1 — Remove the redis service from compose
- `docker-compose.yml`: delete the `redis:` service block (`image: redis:7-alpine` + its
comment) so only `api`, `mariadb`, and the `volumes:` block remain. The api `depends_on`
lists only `mariadb` — leave it untouched.
- `docker-compose.dev.yml`: delete the `redis:` ports override block (`ports: - '6379:6379'`).
- verify: `grep -ri redis docker-compose.yml docker-compose.dev.yml` returns nothing.
- done: neither compose file references redis; `docker compose config` still parses.
### Task 2 — Clean documentation references
Remove/adjust every Redis mention so no doc implies Redis is part of the stack:
- `CLAUDE.md`: delete the Constraints line "Redis available (optional, …)"; delete the
`ioredis` row from the Supporting Libraries table; delete the "Redis is present in the
stack but not yet used…" sentence from the architecture paragraph (keep the rest of the
sentence about SSE/EventEmitter); change the compose-tree comment `(api + mariadb + redis)`
`(api + mariadb)`.
- `README.md`: drop "Redis" from the prerequisites line; drop `redis` from the
`docker compose … up mariadb redis` command; drop ", Redis 7" from the compose-file
description; change "expose DB/Redis ports" → "expose DB ports"; change the Live-sync row
"Server-Sent Events + Redis 7 pub/sub" → "Server-Sent Events (in-process EventEmitter)".
- `docs/ARCHITECTURE.md`: delete the `Redis` table row.
- `docs/CONFIGURATION.md`: drop "and Redis on `localhost:6379`".
- `docs/deployment.md`: delete the `redis` services-table row.
- `docs/DEVELOPMENT.md`: remove the four Redis mentions (prereq bullet, "### 2. Start the dev
database and Redis" heading → "Start the dev database", the two `up mariadb redis` commands
`up mariadb`, the "Exposes Redis on `localhost:6379`" bullet, and the
"(API in Docker + MariaDB + Redis…)" comment → "(API in Docker + MariaDB…)").
- `docs/GETTING-STARTED.md`: prereq row "Used to run MariaDB and Redis locally" → "MariaDB";
`up -d mariadb redis``up -d mariadb`; "and Redis (`localhost:6379`)" removed from prose.
- `docs/TESTING.md`: "The API, MariaDB, and Redis must already be running" → "The API and
MariaDB must already be running".
- `apps/pwa/e2e/README.md`: delete the "- Redis on `:6379`" bullet.
- `apps/pwa/playwright.config.ts`: update the two comments listing
"API+MariaDB+Redis are compose-managed" → "API+MariaDB are compose-managed".
- verify: `grep -rniE redis CLAUDE.md README.md docs apps/pwa/e2e apps/pwa/playwright.config.ts`
returns nothing (case-insensitive, excluding the word "credential").
- done: no doc/config implies Redis is in the stack.
## Constraint — KEEP these (do NOT touch)
The in-memory-vs-Redis **design-rationale** comments document why in-memory is used instead
of Redis (decisions D-12/D-18) and must remain:
- `apps/api/src/lib/listEmitter.ts`
- `apps/api/src/broker/reminderScheduler.ts`
- `apps/api/src/auth/linkNonceStore.ts`
- `apps/api/src/routes/localAuth.ts`
## must_haves
- truth: "No redis service exists in either compose file"
- truth: "No documentation or e2e config references Redis as part of the stack"
- truth: "The D-12/D-18 in-memory-vs-Redis rationale comments in the 4 source files are intact"
- artifacts: [docker-compose.yml, docker-compose.dev.yml, CLAUDE.md, README.md, docs/*, apps/pwa/e2e/README.md, apps/pwa/playwright.config.ts]
@@ -0,0 +1,65 @@
---
quick_id: 260618-smr
slug: remove-unused-redis-service-and-referenc
description: Remove unused Redis service and references
status: complete
completed: 2026-06-18
duration: ~10m
tasks_completed: 2
tasks_total: 2
files_modified:
- docker-compose.yml
- docker-compose.dev.yml
- CLAUDE.md
- README.md
- docs/ARCHITECTURE.md
- docs/CONFIGURATION.md
- docs/deployment.md
- docs/DEVELOPMENT.md
- docs/GETTING-STARTED.md
- docs/TESTING.md
- apps/pwa/e2e/README.md
- apps/pwa/playwright.config.ts
decisions:
- Redis dropped entirely from compose and all docs; in-memory EventEmitter is the live-sync mechanism (D-12/D-18 rationale comments in source preserved)
---
# Quick Task 260618-smr: Remove unused Redis service and references — Summary
## One-liner
Dropped the unused `redis:7-alpine` compose service and scrubbed every documentation/config reference implying Redis is part of the stack, while preserving the D-12/D-18 in-memory-vs-Redis design-rationale comments in source.
## Tasks Completed
| Task | Description | Commit | Files |
| ---- | ------------------------------------ | ------- | ---------------------------------------------------------- |
| 1 | Remove redis service from compose | 269e474 | docker-compose.yml, docker-compose.dev.yml |
| 2 | Clean documentation references | 8255be6 | CLAUDE.md, README.md, docs/*, apps/pwa/e2e/README.md, apps/pwa/playwright.config.ts |
## Verification Results
- `grep -ri redis docker-compose.yml docker-compose.dev.yml` → no output (clean)
- `docker compose -f docker-compose.yml -f docker-compose.dev.yml config` → PARSE OK
- `grep -rniE redis CLAUDE.md README.md docs apps/pwa/e2e apps/pwa/playwright.config.ts` → no output (clean)
- `pnpm format:check` → all matched files use Prettier code style
- Protected source files (listEmitter.ts, reminderScheduler.ts, linkNonceStore.ts, localAuth.ts) → untouched (git diff confirms no changes)
## Docker Compose Config
`docker compose -f docker-compose.yml -f docker-compose.dev.yml config` parsed successfully with no Redis service — confirmed available on this host.
## Deviations from Plan
None — plan executed exactly as written.
## Known Stubs
None.
## Self-Check: PASSED
- Commits 269e474 and 8255be6 exist in git log
- All 12 modified files confirmed updated
- Grep verifies zero Redis references in target files
- Protected source files confirmed untouched
+2 -4
View File
@@ -11,7 +11,6 @@ FamilySync is a self-hosted, Dockerized family organization hub for a two-person
### Constraints
- **Tech stack**: MariaDB for the database — PostgreSQL is not available in the stack
- **Tech stack**: Redis available (optional, for live list sync / push)
- **Infrastructure**: Unraid host running Docker + Docker Compose
- **Auth**: Authelia (already deployed) + Local Auth — OIDC/OAuth2 for the custom app; all members authenticate through it
- **Calendar backend**: Fastmail (paid, existing) is the single source for all calendars via JMAP/CalDAV
@@ -50,7 +49,6 @@ FamilySync is a self-hosted, Dockerized family organization hub for a two-person
| web-push | 3.6.7 | Server-side VAPID push | Generate VAPID keys, sign and dispatch push messages to browser push services (APNs for iOS, FCM for Android) |
| @hono/oidc-auth | 1.8.3 | OIDC session middleware for Hono | Storage-less JWT session cookies; authorization-code + PKCE flow; works with any RFC-compliant OIDC provider including Authelia |
| openid-client | 6.8.4 | Low-level OIDC primitives | If `@hono/oidc-auth` proves insufficient (e.g., custom token introspection), use this as the lower-level escape hatch |
| ioredis | 5.11.0 | Redis client | Pub/sub for broadcasting list-change events to SSE connections across Node processes |
| zod | 3.24.x | Schema validation | Validate API request bodies and CalDAV event payloads before writing back to Fastmail |
| @hono/zod-validator | 0.8.0 | Hono middleware for Zod | Validate request body/query in route handlers with Zod schemas |
| @tanstack/react-query | 5.101.0 | Server state + caching | Manages calendar and list data fetching, background refetch, stale-while-revalidate; pairs with SSE for live list updates |
@@ -199,7 +197,7 @@ The backend handles two auth paths: local username/password (scrypt + HS256 JWT
Calendar data lives exclusively in Fastmail CalDAV. The broker layer (`apps/api/src/broker/`) uses `tsdav` for PROPFIND/REPORT/PUT/DELETE, `ical.js` for VCALENDAR parsing, and `rrule` for server-side recurrence expansion. Writes are enqueued in a `calendarOutbox` table and drained asynchronously every 15 seconds; a ctag-based poller re-syncs calendars every 5 minutes.
Lists are persisted in MariaDB. Live list updates flow over SSE (`text/event-stream`) via an in-process Node.js `EventEmitter`; a 30-second polling fallback is always active. Push notifications (reminders + calendar change alerts) are dispatched via `web-push` (VAPID) to APNs/FCM. Redis is present in the stack but not yet used at runtime (reserved for future multi-process pub/sub).
Lists are persisted in MariaDB. Live list updates flow over SSE (`text/event-stream`) via an in-process Node.js `EventEmitter`; a 30-second polling fallback is always active. Push notifications (reminders + calendar change alerts) are dispatched via `web-push` (VAPID) to APNs/FCM.
The PWA uses TanStack Query for all server state (events, lists, user, sync status, auth mode) and Zustand for UI-only state (selected date, open panels, active tab).
@@ -221,7 +219,7 @@ familysync/
│ ├── hooks/ # useListSSE, usePushSubscription
│ ├── store/ # Zustand stores (calendarStore, listsStore)
│ └── sw.ts # Custom Workbox service worker
├── docker-compose.yml # Production stack (api + mariadb + redis)
├── docker-compose.yml # Production stack (api + mariadb)
└── docker-compose.dev.yml # Dev overrides
```
+5 -5
View File
@@ -15,7 +15,7 @@ A self-hosted family organization hub for a two-person household. One color-code
- Node.js 22 LTS
- pnpm 11.5.1 (`corepack enable pnpm`)
- Docker + Docker Compose (for MariaDB, Redis, and production deployment)
- Docker + Docker Compose (for MariaDB and production deployment)
## Installation
@@ -53,7 +53,7 @@ Required environment variables (set in `.env` or your Docker host):
```bash
# Start backing services
docker compose -f docker-compose.yml -f docker-compose.dev.yml up mariadb redis
docker compose -f docker-compose.yml -f docker-compose.dev.yml up mariadb
# Run migrations
pnpm --filter @familysync/api db:migrate
@@ -79,8 +79,8 @@ The API listens on port 3000. The PWA build is served separately (Vite `preview`
apps/
api/ Hono backend — CalDAV sync, OIDC auth, lists API, push notifications
pwa/ React 19 PWA — calendar view, lists UI, service worker
docker-compose.yml Production services (API, MariaDB 11, Redis 7)
docker-compose.dev.yml Dev overrides (bind-mount src/, expose DB/Redis ports)
docker-compose.yml Production services (API, MariaDB 11)
docker-compose.dev.yml Dev overrides (bind-mount src/, expose DB ports)
```
## Commands
@@ -110,7 +110,7 @@ docker-compose.dev.yml Dev overrides (bind-mount src/, expose DB/Redis ports)
| Auth | `@hono/oidc-auth` 1.8.3 — authorization code + PKCE against Authelia |
| Calendar | tsdav 2.2.2 (CalDAV) + ical.js 2.2.1 against Fastmail |
| Push | web-push 3.6.7 (VAPID) |
| Live sync | Server-Sent Events + Redis 7 pub/sub |
| Live sync | Server-Sent Events (in-process EventEmitter) |
| Frontend | React 19, Vite 8, vite-plugin-pwa 1.3, TanStack Query 5, Zustand 5 |
| Calendar UI | Schedule-X 4.6 |
-1
View File
@@ -15,7 +15,6 @@ The stack must include:
- API on `:3000` started with `DEV_AUTH_BYPASS=true` (see Security Guardrail below)
- PWA dev server on `:5173` (`pnpm --filter @familysync/pwa dev`)
- Dev MariaDB on `:3306` (exposed via `docker-compose.dev.yml`)
- Redis on `:6379`
**`DEV_AUTH_BYPASS=true` MUST be set in the API's environment BEFORE the API process starts.** The harness cannot inject it at runtime — the API reads the env var once at startup. If the API is running without it, all `/api/*` requests return an auth redirect and every spec fails.
+2 -2
View File
@@ -5,7 +5,7 @@
* Auth: DEV_AUTH_BYPASS=true on the API (never storageState D-01/Pitfall 14)
* SW: serviceWorkers: 'block' on all profiles (D-02/Pitfall 15)
* baseURL: env-driven PLAYWRIGHT_BASE_URL (D-08/Rule 8)
* webServer: manages Vite only API+MariaDB+Redis stay compose-managed (D-10)
* webServer: manages Vite only API+MariaDB stay compose-managed (D-10)
*
* Run:
* pnpm --filter @familysync/pwa test:e2e
@@ -63,7 +63,7 @@ export default defineConfig({
},
],
// D-10: manage Vite only; API+MariaDB+Redis are compose-managed
// D-10: manage Vite only; API+MariaDB are compose-managed
// reuseExistingServer: reuse operator's pnpm dev locally; start fresh in CI
webServer: {
command: 'pnpm --filter @familysync/pwa dev',
-4
View File
@@ -23,7 +23,3 @@ services:
mariadb:
ports:
- '3306:3306'
redis:
ports:
- '6379:6379'
-4
View File
@@ -50,9 +50,5 @@ services:
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
# Phase 1: present but unused; Phase 4 wires pub/sub for live list sync
volumes:
mariadb_data:
-1
View File
@@ -248,7 +248,6 @@ routes/setup.ts ──→ db (app_config)
| Local auth storage | scrypt PHC hash in `local_credentials.password_hash`; session signed with `LOCAL_SESSION_SECRET` env var |
| Push notifications | `web-push` (VAPID) → APNs (iOS) / FCM (Android) |
| Live list sync | In-process Node.js `EventEmitter` → SSE (`text/event-stream`) |
| Redis | Present in stack (image: `redis:7-alpine`); not used in current runtime (reserved for future multi-process pub/sub) |
| PWA | React 19 + Vite 8 + `vite-plugin-pwa` (Workbox `injectManifest` mode) |
| Networking | Pangolin/Newt tunnel — no open ports; split-DNS internal domain |
| Deployment | Docker Compose on Unraid; single `api` container serves both the API and the PWA static build |
+1 -1
View File
@@ -163,7 +163,7 @@ 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:
The dev Docker Compose override (`docker-compose.dev.yml`) exposes MariaDB on `localhost:3306`. To run the API and PWA directly on the host:
```bash
# Build the API first (dev script runs compiled output)
+6 -7
View File
@@ -35,7 +35,7 @@ src/
- **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
- **Docker + Docker Compose** — for MariaDB in dev
- **TypeScript 5.x** — installed per-workspace as a dev dependency
## Local Setup
@@ -48,13 +48,13 @@ pnpm install
This installs all workspace packages (`apps/api` and `apps/pwa`) in a single pass.
### 2. Start the dev database and Redis
### 2. Start the dev database
```bash
docker compose -f docker-compose.yml -f docker-compose.dev.yml up mariadb redis -d
docker compose -f docker-compose.yml -f docker-compose.dev.yml up mariadb -d
```
The dev override (`docker-compose.dev.yml`) exposes MariaDB on `localhost:3306` and Redis on `localhost:6379`.
The dev override (`docker-compose.dev.yml`) exposes MariaDB on `localhost:3306`.
### 3. Configure environment variables
@@ -238,17 +238,16 @@ Migration files live in `apps/api/src/db/migrations/` and are committed to versi
## Docker Compose Dev Stack
```bash
# Bring up the full dev stack (API in Docker + MariaDB + Redis, with ports exposed)
# Bring up the full dev stack (API in Docker + MariaDB, 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
docker compose -f docker-compose.yml -f docker-compose.dev.yml up mariadb -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`
+3 -3
View File
@@ -12,7 +12,7 @@ This guide walks from a fresh clone to a running local development environment.
| ----------------------- | ------------------ | ---------------------------------------------------------------------------------- |
| Node.js | `22 LTS` | Matches the `node:22-alpine` base in `apps/api/Dockerfile` |
| pnpm | `11.5.1` | Pinned in `package.json` `packageManager` field; enable via `corepack enable pnpm` |
| Docker + Docker Compose | Any recent version | Used to run MariaDB and Redis locally |
| Docker + Docker Compose | Any recent version | Used to run MariaDB locally |
**Node version management:** If you use nvm or fnm, install Node 22 LTS and set it as the default before continuing. There is no `.nvmrc` in the repo; the target version comes from the Dockerfile.
@@ -72,10 +72,10 @@ Open `.env` and fill in the required values. See [docs/CONFIGURATION.md](CONFIGU
### 5. Start the database services
```bash
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d mariadb redis
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d mariadb
```
This starts MariaDB (bound to `localhost:3306`) and Redis (`localhost:6379`) using the dev override. Wait for MariaDB to pass its health check before proceeding.
This starts MariaDB (bound to `localhost:3306`) using the dev override. Wait for MariaDB to pass its health check before proceeding.
### 6. Run database migrations
+1 -1
View File
@@ -89,7 +89,7 @@ pnpm --filter @familysync/pwa test:e2e:ui
pnpm --filter @familysync/pwa test:e2e:headed
```
The `baseURL` is driven by `PLAYWRIGHT_BASE_URL` (default: `http://localhost:5173`). In local mode the config reuses a running Vite dev server; in CI it starts Vite itself. The API, MariaDB, and Redis must already be running via Docker Compose before launching e2e tests locally — see `docs/DEVELOPMENT.md`.
The `baseURL` is driven by `PLAYWRIGHT_BASE_URL` (default: `http://localhost:5173`). In local mode the config reuses a running Vite dev server; in CI it starts Vite itself. The API and MariaDB must already be running via Docker Compose before launching e2e tests locally — see `docs/DEVELOPMENT.md`.
### Type checking (separate from tests — required)
+4 -5
View File
@@ -16,11 +16,10 @@ Self-hosted Docker deployment on Unraid behind Authelia OIDC and a Pangolin/Newt
The production compose file brings up three services:
| Service | Image | Purpose |
| --------- | ---------------------------------------------------- | ---------------------------------------------------------- |
| `api` | Built from `apps/api/Dockerfile` target `production` | Hono API + compiled React PWA, listens on port 3000 |
| `mariadb` | `mariadb:11` | Persistent MariaDB database |
| `redis` | `redis:7-alpine` | Present for live list sync (pub/sub); unused until Phase 4 |
| Service | Image | Purpose |
| --------- | ---------------------------------------------------- | --------------------------------------------------- |
| `api` | Built from `apps/api/Dockerfile` target `production` | Hono API + compiled React PWA, listens on port 3000 |
| `mariadb` | `mariadb:11` | Persistent MariaDB database |
---