Skip to content

Unify frontend runtime config delivery through backend-served /config.js #308

Description

@mushrafmim

Current Limitation

The officer-portal frontend split runtime configuration across two disconnected mechanisms: Vite's import.meta.env (VITE_* vars baked at build time / read from .env in dev) and window.__APP_CONFIG__, populated by a static public/runtime-env.js placeholder that the Go backend overwrote per-request, but only in production. runtimeConfig.ts's getEnv had to fall back between both sources, and this dual-source design had real consequences:

  • Local dev and production resolved config completely differently, with no way to verify parity between them.
  • The backend never actually served /runtime-env.js in native dev — each agency's dev config.yaml set web.dir: /nonexistent, which disabled the entire route registration (SPA serving and runtime-env serving together) — so config.yaml's web.runtime section (branding, IdP settings, etc.) sat unused for every dev agency.
  • Running multiple agencies in parallel via start-dev.sh all relied entirely on start-dev.sh exporting 8 separate VITE_* env vars per agency into each pnpm run dev process. That config lived only in a bash script, duplicated from (and able to drift from) each agency's backend config.yaml.
  • While investigating a related question (branding), we found frontend/public/configs/<agency>.branding.json files are gitignored and are never generated anywhere in the actual build/deploy pipeline (CI workflows, root Dockerfile, Helm chart, compose) — so every real agency deployment today silently falls back to generic "default" branding instead of that agency's actual logo/favicon/portal name.

Suggested Improvement

Unify all frontend runtime configuration onto a single backend-served endpoint, /config.js (renamed from /runtime-env.js), sourced from each agency's backend/config/<agency>/config.yaml web.runtime section — the same source of truth in dev and prod alike:

  • Backend (internal/web, cmd/server/main.go): decoupled /config.js serving from SPA-asset serving. web.NewHandler no longer errors when the SPA asset dir is missing — it now always serves /config.js, and only conditionally serves the SPA (Handler.ServesSPA()) when the asset dir actually exists. Config.Validate() now runs unconditionally at startup, so web.runtime must be valid in every deployment, dev included.
  • Backend config: populated web.runtime in all 6 per-agency dev config.yaml files (branding name, API/IdP URLs, client id, expected OU, scopes) — values that previously lived only in start-dev.sh.
  • Frontend (runtimeConfig.ts): dropped the import.meta.env fallback entirely — getEnv now reads only window.__APP_CONFIG__.
  • Frontend (vite.config.ts): the dev server now proxies /config.js to the paired backend instance (target: VITE_API_BASE_URL), so dev reads the exact same live config the backend serves in prod. Running several agencies side by side via start-dev.sh all still gets each the right config, since each dev server process proxies to its own backend instance.
  • start-dev.sh: simplified — start_frontend now only passes VITE_PORT and VITE_API_BASE_URL (the proxy target) instead of 8 separate VITE_* vars; the CONFIG_* table dropped the now-unused IDP_CLIENT_ID/OU_HANDLE fields.
  • Renamed /runtime-env.js/config.js throughout (Dockerfile, docs/deployment-openshift.md, backend comments/tests, config.example.yaml).

Verified live end-to-end during development: booted the npqs backend on a scratch port, confirmed /config.js served the correct per-agency payload sourced from config.yaml, then pointed a Vite dev server at it and confirmed the proxied /config.js matched exactly, with index.html's <script src="/config.js"> intact. go build/go vet/go test ./... all pass; frontend typechecks clean on both tsconfig.app.json and tsconfig.node.json.

Not yet implemented — flagged during design discussion as follow-ups:

  1. Branding content (frontend/public/configs/*.branding.json) is still a separate, currently-broken static-file mechanism. Folding it directly into web.runtime/config.js was discussed as the fix (reusing the one delivery pipeline that actually works end-to-end in this repo), but not yet built.
  2. A future "configurable consignment table columns" feature should not go through config.yaml/config.js if it needs to surface dynamic custom_data fields — those are already defined per-task-config via the artifact-loader system (consignmentFields, consignmentCustomDataSchemaPath), and duplicating that list into config.yaml would create a second, driftable source of truth for what fields exist.

Version

Commit 5f0180a (HEAD of main) — the changes described above are uncommitted working-tree changes on top of this commit.

Additional Context

Changed files (uncommitted): Dockerfile, backend/cmd/server/main.go, backend/config.example.yaml, backend/config/{npqs,fcau,cda,slpa,customs,sltb}/config.yaml, backend/internal/web/{config.go,config_test.go,handler.go}, docs/deployment-openshift.md, frontend/.env.example, frontend/index.html, frontend/public/runtime-env.jsfrontend/public/config.js (renamed), frontend/src/runtimeConfig.ts, frontend/vite.config.ts, start-dev.sh.

This issue tracks landing this work as a PR, plus the two follow-ups noted above.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type/ImprovementEnhancement to existing functionality

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions