Skip to content

Support Runtime Dynamic Configuration for SmartForms #1780

@jgsuess

Description

@jgsuess

Is your feature request related to a problem? Please describe.

SmartForms currently relies on Vite build-time environment variables (import.meta.env). This means any change to server URLs, launch parameters, or feature flags requires a full rebuild of the application. In Kubernetes or containerised deployments where the same image is promoted across multiple environments (dev → test → prod), this approach is inefficient and complicates CI/CD workflows.


Describe the solution you'd like

Add support for runtime dynamic configuration loading:

  • Provide a mechanism to fetch a config.json file at application start-up before rendering the UI.
  • Merge these runtime values with import.meta.env and sensible defaults.
  • Document a Kubernetes deployment pattern where config.json is mounted or generated from a ConfigMap.
  • Include a TypeScript helper (loadRuntimeConfig()) to load and validate configuration before bootstrapping the app.

This allows environment-specific values to be applied without rebuilding the SPA.


Describe alternatives you've considered

  • Option 2B: Inject window.__RUNTIME_CONFIG__ via script
    Works well but requires modifying index.html and container entrypoints. Option 2A is simpler for static hosting (S3, CloudFront).

  • Rebuilding per environment
    Current approach but increases build complexity and slows deployments.

  • Reverse proxy routing only
    Useful for API host changes but does not cover other variables like OAuth scope or feature flags.


Additional context

Variables to support


VITE\_ONTOSERVER\_URL
VITE\_FORMS\_SERVER\_URL
VITE\_LAUNCH\_SCOPE
VITE\_LAUNCH\_CLIENT\_ID
VITE\_IN\_APP\_POPULATE
VITE\_PRESERVE\_SYM\_LINKS
MODE


1) Deployment Pattern (Kubernetes)

Mount or generate config.json from a ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: smartforms-runtime-config
data:
  config.json: |
    {
      "VITE_ONTOSERVER_URL": "https://ontoserver.prod.example.com",
      "VITE_FORMS_SERVER_URL": "https://forms.prod.example.com",
      "VITE_LAUNCH_SCOPE": "openid profile",
      "VITE_LAUNCH_CLIENT_ID": "smartforms-client",
      "VITE_IN_APP_POPULATE": true,
      "VITE_PRESERVE_SYM_LINKS": false,
      "MODE": "production"
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: smartforms
spec:
  template:
    spec:
      volumes:
        - name: config-volume
          configMap:
            name: smartforms-runtime-config
      containers:
        - name: nginx
          image: aehrc/smart-forms:latest
          volumeMounts:
            - name: config-volume
              mountPath: /usr/share/nginx/html/config.json
              subPath: config.json

2) TypeScript Helper (src/config/runtime.ts)

export interface SmartFormsConfig {
  VITE_ONTOSERVER_URL: string;
  VITE_FORMS_SERVER_URL: string;
  VITE_LAUNCH_SCOPE: string;
  VITE_LAUNCH_CLIENT_ID: string;
  VITE_IN_APP_POPULATE: boolean;
  VITE_PRESERVE_SYM_LINKS: boolean;
  MODE: string;
}

let cfg: SmartFormsConfig | null = null;

export async function loadRuntimeConfig(): Promise<SmartFormsConfig> {
  if (cfg) return cfg;
  const res = await fetch(`${import.meta.env.BASE_URL}config.json`, { cache: 'no-cache' });
  if (!res.ok) throw new Error('Failed to load runtime config');
  const json = await res.json();
  cfg = {
    VITE_ONTOSERVER_URL: json.VITE_ONTOSERVER_URL ?? import.meta.env.VITE_ONTOSERVER_URL,
    VITE_FORMS_SERVER_URL: json.VITE_FORMS_SERVER_URL ?? import.meta.env.VITE_FORMS_SERVER_URL,
    VITE_LAUNCH_SCOPE: json.VITE_LAUNCH_SCOPE ?? import.meta.env.VITE_LAUNCH_SCOPE,
    VITE_LAUNCH_CLIENT_ID: json.VITE_LAUNCH_CLIENT_ID ?? import.meta.env.VITE_LAUNCH_CLIENT_ID,
    VITE_IN_APP_POPULATE: json.VITE_IN_APP_POPULATE ?? (import.meta.env.VITE_IN_APP_POPULATE === 'true'),
    VITE_PRESERVE_SYM_LINKS: json.VITE_PRESERVE_SYM_LINKS ?? (import.meta.env.VITE_PRESERVE_SYM_LINKS === 'true'),
    MODE: json.MODE ?? import.meta.env.MODE ?? 'development'
  };
  return cfg;
}

Usage in main.tsx:

import ReactDOM from 'react-dom/client';
import App from './App';
import { loadRuntimeConfig } from './config/runtime';

loadRuntimeConfig()
  .then(() => ReactDOM.createRoot(document.getElementById('root')!).render(<App />))
  .catch(err => console.error('Config load failed', err));

3) Example config.json

{
  "VITE_ONTOSERVER_URL": "https://ontoserver.prod.example.com",
  "VITE_FORMS_SERVER_URL": "https://forms.prod.example.com",
  "VITE_LAUNCH_SCOPE": "openid profile",
  "VITE_LAUNCH_CLIENT_ID": "smartforms-client",
  "VITE_IN_APP_POPULATE": true,
  "VITE_PRESERVE_SYM_LINKS": false,
  "MODE": "production"
}

4) Unit Tests (Vitest)

import { describe, it, expect, vi } from 'vitest';
import { loadRuntimeConfig } from './runtime';

describe('loadRuntimeConfig()', () => {
  it('loads config.json and merges with env', async () => {
    const mockJson = {
      VITE_ONTOSERVER_URL: 'https://rt.onto',
      VITE_FORMS_SERVER_URL: 'https://rt.forms',
      VITE_IN_APP_POPULATE: true
    };
    global.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => mockJson });
    const cfg = await loadRuntimeConfig();
    expect(cfg.VITE_ONTOSERVER_URL).toBe('https://rt.onto');
    expect(cfg.VITE_FORMS_SERVER_URL).toBe('https://rt.forms');
    expect(cfg.VITE_IN_APP_POPULATE).toBe(true);
  });

  it('throws if fetch fails', async () => {
    global.fetch = vi.fn().mockResolvedValue({ ok: false });
    await expect(loadRuntimeConfig()).rejects.toThrow();
  });
});

Acceptance Criteria

  • App fetches config.json before rendering.
  • Fallback order works: config.jsonimport.meta.env → defaults.
  • Kubernetes manifest and helper documented.
  • Unit tests pass in CI.

✅ This enhancement makes SmartForms Kubernetes-friendly, reduces rebuild overhead, and supports dynamic configuration for Ontoserver, Forms server, and OAuth parameters using a simple config.json pattern.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions