# Daily Quote A small full-stack "quote of the day" app, built for the privatecloud VM `private-cloud-daily-quote`. ## Stack - **Frontend**: Vite + React, styled with [MUI](https://mui.com) using a Material Design 3 theme (color roles, type scale, shape/elevation). - **API**: Express (Node.js), serving both `/api/*` and the built SPA on port **3000**. - **Database**: Postgres 16, run via Docker Compose (`quotes`, `users`, `favorites` tables — see `db/init.sql`). - **Platform auth**: [Zitadel](https://zitadel.com), self-hosted via Docker, OIDC Authorization Code + PKCE. Gates "submit a quote" and "favorites"; browsing is public. - **Git hosting**: [Gitea](https://gitea.io), self-hosted via Docker (this repo's `origin`). - **CI/CD**: Gitea Actions, using a local `act_runner` container. See `.gitea/workflows/ci.yml`. Verified working end-to-end (checkout → build → syntax check, all green), but the runner has occasionally been slow to pick up a freshly-pushed task on this VM; a `docker restart quoteapp-gitea-runner` reliably kicks it back into gear if a run seems stuck in "queued". - **Observability**: `prom-client` metrics at `/api/metrics`, scraped by Prometheus, visualized in a provisioned Grafana dashboard. - **Backup**: nightly `pg_dump` via a systemd timer (`quoteapp-backup.timer`), 7-day retention, dumps to `~/backups`. ## Local services (Docker) | Service | Internal port | Host port | Notes | |-------------|---------------|-----------|-------| | Postgres | 5432 | 5432 (loopback only) | app + zitadel databases | | Zitadel | 8080 | 8080 | public via `https://:8080` | | Gitea | 3000 | 3001 | public via `https://:3001` | | Gitea SSH | 22 | 2222 (loopback only) | | | act_runner | - | - | registered against Gitea Actions | | Prometheus | 9090 | 9090 (loopback only) | | | Grafana | 3000 | 3002 | public via `https://:3002`, `admin`/`admin` | Bring the stack up with: ```sh docker compose up -d ``` ## Running the app The app itself runs directly on the VM (not in Docker) as a systemd service on port 3000: ```sh sudo systemctl status quote-app ``` To rebuild after a frontend change: ```sh cd client && npm run build sudo systemctl restart quote-app ``` ## Auth notes Zitadel is reachable at `https://:8080`. Since exe.dev's proxy only marks one port ("3000", the app) fully public, signing in requires being an authorized user of this workspace to reach the Zitadel login page on port 8080 — browsing and reading quotes on the main app is public regardless. A demo user (`demo`) was created for testing; see `.zitadel-demo-user` (not committed) for its password. Zitadel defaults to "Login V2", a separate UI app we don't deploy here, so the instance's `loginV2.required` feature flag was switched off in favor of the classic login UI bundled in the main server image (`ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED` in `docker-compose.yml`, plus a one-time `PUT /v2/features/instance` call against the already-running instance — re-apply that call if the Zitadel volume is ever recreated and the env var doesn't take retroactively). The full authorize → login → callback → session chain was verified as far as it can be from inside the VM: OIDC discovery, the PKCE authorize redirect, and the login page itself (200, not 404) all check out. The very last mile — an actual browser completing login — couldn't be driven from here, since this VM's own DNS resolves its public hostname to a private, TLS-less address (bypassing exe.dev's real edge proxy), and Zitadel's login cookie is `__Host-`-prefixed, which browsers only accept over genuine HTTPS. Worth a real sign-in test from an actual browser to confirm the last step. ## Backups ```sh ./backup/backup.sh # run manually systemctl list-timers quoteapp-backup.timer ```