How to Manage API Keys in a Browser Application: The Vite + Window Pattern

The gods-eye-view project secures sensitive credentials by storing API keys in server-side environment variables, selectively injecting only necessary keys into the browser via Vite's import.meta.env, and validating user input through strict sanitization rules.

Managing API keys in client-side JavaScript presents a fundamental security challenge: browsers cannot keep secrets. The gods-eye-view repository solves this by isolating keys on the server, deliberately surfacing only the Google Maps API key to the client, and wrapping access in validation layers. This approach ensures that sensitive keys like OPENAI_API_KEY and AISSTREAM_API_KEY remain server-side while allowing the frontend to access mapping functionality.

Server-Side Storage and Selective Exposure

The architecture begins with Vite's environment variable system. All sensitive keys—including GOOGLE_MAPS_API_KEY, TOMTOM_API_KEY, OPENAI_API_KEY, and AISSTREAM_API_KEY—reside in a .env file that never leaves the server. The Vite configuration reads these via process.env.* and makes selective decisions about client exposure.

Server-only keys remain protected. For example, the configuration checks for AISSTREAM_API_KEY and returns a 503 status if missing, preventing the application from starting without proper credentials (vite.config.js#L4894). Similarly, OPENAI_API_KEY is strictly server-side, with endpoints verifying its presence before processing requests (vite.config.js#L5073).

Client-side exposure is explicit. Only the Google Maps key gets injected into the browser bundle. The build pipeline uses import.meta.env to pass this specific variable through, while stripping others from the client build (vite.config.js#L7779).

Runtime Key Management Pattern

Exposing Keys to the Window Object

The entry point src/main.js receives the selectively exposed key and attaches it to the global window object for downstream consumption:

// src/main.js
const googleApiKey = import.meta.env.GOOGLE_MAPS_API_KEY;
if (googleApiKey) {
  window.__GOOGLE_MAPS_API_KEY__ = googleApiKey;
}

This pattern appears at lines 81-82, creating a clear contract: browser code expects window.__GOOGLE_MAPS_API_KEY__ to contain the valid key.

Runtime Retrieval with Fallback

Client modules implement a defensive retrieval pattern that checks the global window variable first, then falls back to import.meta.env for direct access:

function getGoogleMapsKey() {
  return window.__GOOGLE_MAPS_API_KEY__ || import.meta.env.GOOGLE_MAPS_API_KEY;
}

// Usage in locations module
export async function fetchLocationData(lat, lng) {
  const key = getGoogleMapsKey();
  if (!key) throw new Error('Google Maps API key not configured');
  
  const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${encodeURIComponent(key)}`;
  // ...
}

This retrieval logic appears in src/locations.js#L350 and src/voice/gevActions.js#L1267.

Server-Side Proxy Patterns

For keys that must remain hidden, the application uses server-side proxying. The TOMTOM API key, for instance, never reaches the browser. Instead, the Vite dev server constructs URLs using process.env.TOMTOM_API_KEY (vite.config.js#L1891) and validates its presence before handling requests (vite.config.js#L1948).

Input Validation and Sanitization

When users supply API keys through the interface (for local development or custom deployments), the application enforces strict validation rules defined in src/keySetupCore.test.mjs. These rules prevent injection attacks and malformed input:

function validateApiKey(key) {
  const trimmed = key.trim();
  
  // Reject empty keys
  if (!trimmed) return false;
  
  // Reject keys with internal whitespace
  if (/\s/.test(trimmed)) return false;
  
  // Reject non-ASCII characters
  if (/[^\x00-\x7F]/.test(trimmed)) return false;
  
  // Enforce maximum length (2000 characters)
  if (trimmed.length > 2000) return false;
  
  return true;
}

The validation suite at lines 111-118 ensures that any key passed to window.__GOOGLE_MAPS_API_KEY__ or stored in .env meets these security criteria.

CLI and Development Tooling

Helper scripts in the tools/ directory read from the same .env file but abort with clear error messages if keys are missing. For example, the Street View panorama tool checks for GOOGLE_MAPS_API_KEY and exits gracefully if absent (tools/streetview-panorama.mjs#L74-L90). The tools/README.md documents the requirement to place the Google Maps key in .env before running CLI operations.

Summary

  • Server-side isolation: Store all API keys in .env and restrict client exposure using Vite's environment filtering.
  • Selective browser exposure: Only expose keys that the browser absolutely requires (e.g., Google Maps) via import.meta.env and window object assignment.
  • Defensive retrieval: Implement fallback patterns in client code: window.__VAR__ || import.meta.env.VAR.
  • Strict validation: Sanitize user-provided keys using regex rules that block whitespace, non-ASCII characters, and overflow attacks.
  • Server proxying: Keep sensitive keys like OPENAI_API_KEY and AISSTREAM_API_KEY server-side, using Vite middleware or backend endpoints to proxy requests.

Frequently Asked Questions

Why can't I expose all API keys to the browser?

Exposing server-side keys like OPENAI_API_KEY or AISSTREAM_API_KEY would allow malicious users to extract them from browser DevTools and abuse your quotas or access restricted data. The gods-eye-view architecture keeps these in process.env exclusively, returning 503 errors if they're missing from the server environment (vite.config.js#L6420-L6421), while only the Google Maps key—required for client-side map rendering—reaches the browser.

How does the application handle missing API keys?

The build system and runtime perform layered checks. At build time, Vite validates that required server keys exist. At runtime, client modules check window.__GOOGLE_MAPS_API_KEY__ and throw descriptive errors if undefined. Server endpoints verify process.env variables and return 500-series status codes if credentials are absent, preventing partial application states.

What validation rules apply to user-provided API keys?

The validation layer enforces ASCII-only input, rejects internal whitespace, trims leading/trailing spaces, and limits length to 2000 characters. These rules, tested in src/keySetupCore.test.mjs, prevent common injection vectors and ensure compatibility with URL encoding when constructing API requests.

How do CLI tools access the same environment variables?

Development scripts use Node.js process.env to read the .env file directly, bypassing the browser exposure layer. The Street View tool and other utilities instantiate a validation check before executing API calls, ensuring that missing keys trigger immediate, clear error messages rather than runtime failures (tools/streetview-panorama.mjs#L74-L90).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →