Daily Quote app
Go to file
Daily Quote Builder 0194518a20
All checks were successful
CI / build (push) Successful in 22s
Zitadel: fall back to Login V1 UI (Login V2 needs a separate deployed app)
2026-07-20 17:10:39 +00:00
.gitea/workflows Daily Quote: Vite/React + MUI M3 frontend, Express API, Postgres, Zitadel auth, observability, backups 2026-07-20 16:43:47 +00:00
backup Daily Quote: Vite/React + MUI M3 frontend, Express API, Postgres, Zitadel auth, observability, backups 2026-07-20 16:43:47 +00:00
client Daily Quote: Vite/React + MUI M3 frontend, Express API, Postgres, Zitadel auth, observability, backups 2026-07-20 16:43:47 +00:00
db Daily Quote: Vite/React + MUI M3 frontend, Express API, Postgres, Zitadel auth, observability, backups 2026-07-20 16:43:47 +00:00
gitea CI: quiet runner logging back to info 2026-07-20 17:00:59 +00:00
observability Daily Quote: Vite/React + MUI M3 frontend, Express API, Postgres, Zitadel auth, observability, backups 2026-07-20 16:43:47 +00:00
server Daily Quote: Vite/React + MUI M3 frontend, Express API, Postgres, Zitadel auth, observability, backups 2026-07-20 16:43:47 +00:00
systemd Daily Quote: Vite/React + MUI M3 frontend, Express API, Postgres, Zitadel auth, observability, backups 2026-07-20 16:43:47 +00:00
.env.example Daily Quote: Vite/React + MUI M3 frontend, Express API, Postgres, Zitadel auth, observability, backups 2026-07-20 16:43:47 +00:00
.gitignore Daily Quote: Vite/React + MUI M3 frontend, Express API, Postgres, Zitadel auth, observability, backups 2026-07-20 16:43:47 +00:00
docker-compose.yml Zitadel: fall back to Login V1 UI (Login V2 needs a separate deployed app) 2026-07-20 17:10:39 +00:00
README.md Zitadel: fall back to Login V1 UI (Login V2 needs a separate deployed app) 2026-07-20 17:10:39 +00:00

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 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, self-hosted via Docker, OIDC Authorization Code + PKCE. Gates "submit a quote" and "favorites"; browsing is public.
  • Git hosting: Gitea, 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://<host>:8080
Gitea 3000 3001 public via https://<host>:3001
Gitea SSH 22 2222 (loopback only)
act_runner - - registered against Gitea Actions
Prometheus 9090 9090 (loopback only)
Grafana 3000 3002 public via https://<host>:3002, admin/admin

Bring the stack up with:

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:

sudo systemctl status quote-app

To rebuild after a frontend change:

cd client && npm run build
sudo systemctl restart quote-app

Auth notes

Zitadel is reachable at https://<vm-host>: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

./backup/backup.sh          # run manually
systemctl list-timers quoteapp-backup.timer