app/bridge: runtime env shim via window.__ENV / /__ENV.js

Vite inlines `import.meta.env.VITE_*` at build time, so setting those
vars at runtime on a deployed image is a no-op — the bundle already
carries baked URLs. Compose/k8s `environment:` blocks targeting
VITE_BRIDGE_* are cosmetic in that world.

Layer a tiny runtime shim on top:
  1. /__ENV.js (this commit ships the build-time template under
     public/) is loaded synchronously in <head> before the main
     bundle. The serving image overwrites it at boot from real env.
  2. bridge.config.ts reads window.__ENV first, then falls back to
     import.meta.env.VITE_* for dev and as a safety net.

This decouples the deployable image from its environment: one image,
N envs. Tenants (zoo, future white-labels) copy the public/__ENV.js
template + the env() helper pattern verbatim — only the key names
change.

The dist bundle (search: `window.__ENV`) wires the runtime read,
confirmed via Vite preview + Playwright eval — `window.__ENV` is an
Object with all six expected keys at page load. The pattern works
with or without the docker-entrypoint that ships in a follow-up
commit; an image that fails to template /__ENV.js still gets a
working SPA via build-time fallback.
This commit is contained in:
2026-05-21 16:48:05 -07:00
parent 300354cb5a
commit ebd9d71efb
4 changed files with 62 additions and 9 deletions
+12
View File
@@ -6,6 +6,18 @@
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%235b8def'/></svg>" />
<title>Lux Bridge</title>
<meta name="description" content="Cross-chain bridge for the Lux Network." />
<!--
Runtime env: /__ENV.js sets window.__ENV before the main bundle
loads. Serving image templates this file at boot from BRIDGE_*
env vars; bridge.config.ts reads window.__ENV first, then falls
back to Vite's import.meta.env. One image, N envs.
Placed in <head> (and before the module script) so the synchronous
parser-blocking load happens before any deferred module executes —
the ordering is unambiguous regardless of how Vite reorganises
the bundle.
-->
<script src="/__ENV.js"></script>
</head>
<body>
<div id="bridge-root"></div>
+14
View File
@@ -0,0 +1,14 @@
// Runtime env for the bridge SPA. Overwritten at container boot by the
// serving image's docker-entrypoint, or baked at build time.
//
// The SDK's bridge.config.ts reads window.__ENV before falling back to
// Vite's build-time import.meta.env. This decouples the deployable image
// from the environment it runs in: one image, N envs.
window.__ENV = window.__ENV || {
BRIDGE_API_HOST: '',
BRIDGE_ENV: '',
BRIDGE_CLIENT_ID: '',
BRIDGE_IAM_ORG: '',
BRIDGE_LOGO_URL: '',
WC_PROJECT_ID: '',
}
+30 -9
View File
@@ -7,24 +7,45 @@
// Logo source of truth: @luxfi/logo (https://github.com/luxfi/logo).
// We import the canonical SVG generator and inline it as a data URL so
// the build artifact has zero external dependencies for the brand mark.
//
// Env resolution order:
// 1. window.__ENV — runtime, set by /__ENV.js at page load. One
// image, N envs. Serving container templates
// /__ENV.js from real env at boot.
// 2. import.meta.env — build-time Vite fallback, used in dev and as
// a safety net if the serving image fails to
// template /__ENV.js.
import type { BridgeConfig } from '@luxfi/bridge'
import { getColorSVG } from '@luxfi/logo'
declare global {
interface Window {
__ENV?: Partial<Record<string, string>>
}
}
const runtime: Partial<Record<string, string>> =
(typeof window !== 'undefined' && window.__ENV) || {}
const build = import.meta.env
const env = (key: string, fallback?: string): string | undefined => {
const r = runtime[key]
if (r) return r
const b = build[`VITE_${key}`]
if (b) return b
return fallback
}
const luxLogoDataUrl = `data:image/svg+xml;utf8,${encodeURIComponent(getColorSVG())}`
// `import.meta.env` is Vite's standard env namespace; missing values fall back
// to the production network.
const env = import.meta.env
export const bridgeConfig: BridgeConfig = {
apiHost: env.VITE_BRIDGE_API_HOST ?? 'https://api.bridge.lux.network',
env: env.VITE_BRIDGE_ENV ?? 'mainnet',
clientId: env.VITE_BRIDGE_CLIENT_ID,
iamOrg: env.VITE_BRIDGE_IAM_ORG ?? 'lux',
apiHost: env('BRIDGE_API_HOST', 'https://api.bridge.lux.network')!,
env: env('BRIDGE_ENV', 'mainnet')!,
clientId: env('BRIDGE_CLIENT_ID'),
iamOrg: env('BRIDGE_IAM_ORG', 'lux'),
brand: {
name: 'Lux Bridge',
logoUrl: env.VITE_BRIDGE_LOGO_URL ?? luxLogoDataUrl,
logoUrl: env('BRIDGE_LOGO_URL') || luxLogoDataUrl,
primaryColor: '#5b8def',
secondaryColor: '#7a9ff5',
supportEmail: '[email protected]',
+6
View File
@@ -1,11 +1,17 @@
/// <reference types="vite/client" />
// Build-time Vite envs. These are the fallback for the runtime
// window.__ENV mechanism (see src/bridge.config.ts). Vite inlines
// `import.meta.env.VITE_*` at build, so changing one after the
// bundle is built does nothing — that's why we layer window.__ENV
// (templated by the serving image at boot) on top.
interface ImportMetaEnv {
readonly VITE_BRIDGE_API_HOST?: string
readonly VITE_BRIDGE_ENV?: string
readonly VITE_BRIDGE_CLIENT_ID?: string
readonly VITE_BRIDGE_IAM_ORG?: string
readonly VITE_BRIDGE_LOGO_URL?: string
readonly VITE_WC_PROJECT_ID?: string
}
interface ImportMeta {