# Local development uses one file for browser and server keys. # Copy to .env (ignored); this example contains names and placeholders only. # Only explicitly selected browser keys are injected into the client bundle. # God's Eye View — environment variables # Copy to .env and fill in your keys. On macOS the launcher can also read keys # from the Keychain (see README); on Linux/Windows use this file or env vars. # # Easiest path: don't edit anything. Run the app and paste keys into the # in-app Provider Settings panel (the POWER UP chip, bottom-right) — it writes # this checkout's .env for you (owner-only permissions) and restarts the dev # server. Under the Pinokio launcher it writes pinokio/ENVIRONMENT instead. # Keys you supply yourself (shell env, Keychain) are shown as configured # externally and never touched. This file remains the reference for headless # and self-hosted setups. # # NOTE ON CLIENT-EXPOSED KEYS: GOOGLE_MAPS_API_KEY, CESIUM_ION_TOKEN and # MAPILLARY_CLIENT_TOKEN are injected into the browser bundle by design # (they're used client-side) and WILL be visible in devtools. Restrict/scope them rather than trying to hide # them (see SECURITY.md). All other keys below stay server-side. # Optional: direct Google Photorealistic 3D Tiles and GEV place search. # Without it, a Cesium ion token can still load ion-hosted Google 3D; with # neither credential, the app starts on keyless Esri World Imagery with OSM # available in the map tray and as the automatic provider-failure fallback. # CLIENT-EXPOSED — restrict it (HTTP referrer + API restriction) in Google Cloud. GOOGLE_MAPS_API_KEY= # Optional: a SEPARATE Google API key for the SERVER-SIDE Google calls — Places # (/api/google/nearby-places, /api/google/text-search) and the CCTV Street View # fallback (#33). It never reaches the browser, so it can be restricted by # server IP (not HTTP referrer) and scoped to just Places API + Street View # Static API, letting the client-exposed key above stay narrowly scoped to Map # Tiles + Geocoding. Unset, these calls fall back to GOOGLE_MAPS_API_KEY, so a # single shared key keeps working — splitting is opt-in. # SERVER-SIDE ONLY. GOOGLE_MAPS_SERVER_API_KEY= # Per-IP rate limit for the Google Places cost endpoints # (/api/google/nearby-places + /api/google/text-search), in requests per minute. # DEFAULTS TO 120, the value the Pinokio build already ships. Raise or lower it # with a positive integer; 60 is a sensible tightening whenever HOST is not # localhost. Set it to exactly 0 to run unthrottled. An unreadable value (a # typo, a stray unit) falls back to the default rather than to unlimited, so a # slip cannot quietly disarm the guard. A positive value below 1 counts as 1. # NOTE: this is a per-IP, process-local guard (in-memory, resets on restart; # over-limit requests get a sanitized 429) — it is NOT a billing cap. For hard # spend protection set provider-side budget alerts: Google Cloud Console -> # Billing -> Budgets & alerts (plus per-API quotas under APIs & Services -> # Quotas). # GEV_RATELIMIT_GOOGLE_PER_MIN=60 # uncomment to tighten from the default 120 # Optional: Google Photorealistic 3D Tiles through Cesium ion, Bing world # imagery, and Cesium World Terrain. The free Community plan is for eligible # personal/non-commercial use and has quotas; check current Cesium terms. A # direct Google key above is still required for GEV place search. # CLIENT-EXPOSED — use a public assets:read token with URL restrictions. CESIUM_ION_TOKEN= # Optional: OpenAI Realtime voice control. API key is the default cloud auth. # Provider Settings can instead opt voice into the signed-in local # ChatGPT/Codex OAuth session; the browser still receives only a short-lived # Realtime client secret. Do not prefix with VITE_. OPENAI_API_KEY= # Optional: forbid API-key voice sessions. Select ChatGPT OAuth in Provider # Settings; missing or expired Codex sign-in then fails closed. # GEV_PREFER_CODEX_OAUTH=true OPENAI_REALTIME_MODEL=gpt-realtime-2 # Cheaper voice tier, used when the GEV MIC panel's MINI toggle is on. # Model ids and prices are external facts that drift — if OpenAI moves this id, # override it here rather than editing src/voice/voiceCost.js. OPENAI_REALTIME_MODEL_MINI=gpt-realtime-2.1-mini OPENAI_REALTIME_VOICE=marin OPENAI_REALTIME_REASONING_EFFORT=low # Keep only a short conversational window. Current map state is fetched live. OPENAI_REALTIME_CONTEXT_TOKENS=3000 OPENAI_REALTIME_CONTEXT_RETENTION=0.5 # Voice-card captions of what you said. Billed separately by OpenAI and # counted in the session cost meter and cap; off disables them. OPENAI_REALTIME_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe # The voice debug log (.gev-logs/realtime-conversations.jsonl, rotated at # 32 MB, never expired) omits transcripts, typed or spoken text, and tool # arguments/results (including serialized outputs) by default. Set to 1 to # keep them while debugging; delete the log when you are done. GEV_VOICE_LOG_CONTENT= OPENAI_HUD_SUMMARY_MODEL=gpt-5-nano # Per-IP rate limit for the OpenAI cost endpoints # (/api/realtime/token + /api/openai/hud-summary), in requests per minute. # DEFAULTS TO 30, the value the Pinokio build also ships. That clears ordinary # use several times over: an open tab asks for a HUD summary every 15s (4/min) # and a voice session mints one token. Raise or lower it with a positive # integer; set it to exactly 0 to run unthrottled. An unreadable value (a typo, # a stray unit) falls back to the default rather than to unlimited, so a slip # cannot quietly disarm the guard. A positive value below 1 counts as 1. # NOTE: this is a per-IP, process-local guard (in-memory, resets on restart; # over-limit requests get a sanitized 429) — it is NOT a billing cap. For hard # spend protection set a provider-side budget: OpenAI platform -> Settings -> # Limits (usage limits). # GEV_RATELIMIT_OPENAI_PER_MIN=30 # the default; uncomment only to change it # Optional: exact socket addresses (comma-separated, no wildcards or prefixes) # that Provider Settings treats as this machine in addition to loopback. Leave # unset. The Docker launcher sets it to the container's gateway at start, # because that is where Docker delivers your own browser from; it is safe only # while the port is published on 127.0.0.1. Read SECURITY.md before setting it # by hand. # GEV_KEY_SETUP_TRUSTED_PEERS= # Optional: Mapillary imagery for the Street Level layer. Free client token from # https://www.mapillary.com/dashboard/developers (register an app, copy its # Client Token). Client-exposed by design, like GOOGLE_MAPS_API_KEY. MAPILLARY_CLIENT_TOKEN= # Optional: OpenSky auth mode (oauth | basic | auto | anon) # Default: oauth — requires client credentials below # Set to "anon" to skip aircraft auth (rate-limited but works) OPENSKY_AUTH_MODE=oauth # Optional: OpenSky OAuth credentials (from opensky-network.org account dashboard) OPENSKY_CLIENT_ID= OPENSKY_CLIENT_SECRET= # Optional: higher Launch Library 2 request allowance. Public access works # without a token. LL2_API_TOKEN= # Optional: OpenSky credentials JSON file path (alternative to above) # OPENSKY_CREDENTIALS_FILE=/path/to/credentials.json # Optional: dev server config PORT=4173 # Network binding. Default is localhost — the server is reachable ONLY from # this machine. This is the safe default: the dev server brokers your # configured API keys, so anyone who can reach it can spend your quota. # LAN opt-in: set HOST=0.0.0.0 to share on your local network. WARNING: that # exposes every key-brokering proxy to everyone on the network — use only on # networks you trust, and consider the GEV_RATELIMIT_* throttles above. # HOST=localhost # HOST=0.0.0.0 # Optional: comma-separated LAN hostnames to accept, in addition to localhost # and IP addresses, for example GEV_ALLOWED_HOSTS=globe.lan,globe.internal. # Binding HOST=0.0.0.0 alone does NOT trust every Host header; suffix and # wildcard entries are ignored. # GEV_ALLOWED_HOSTS= # Optional: NASA FIRMS live active fires. SERVER-SIDE ONLY (never reaches the # browser — the client polls the /api/firms proxy). Free key: # https://firms.modaps.eosdis.nasa.gov/api/map_key/ # Without a key the FIRMS Active Fires layer is empty ("KEY REQUIRED"). # FIRMS_MAP_KEY= # Optional: Live AIS vessels layer # Browser fetch path. Keep same-origin so the private stream key stays server-side. VITE_AIS_LIVE_API_URL=/api/vessels # Client render cap. The server can keep a larger AISStream cache, but drawing # every global vessel at once can overwhelm WebGL on laptops. VITE_AIS_LIVE_MAX_ROWS=12000 # Active label cap after screen-space clustering. Labels exist for all rendered # ships, but only clustered winners are shown to keep text readable. VITE_AIS_LIVE_LABEL_MAX_ROWS=900 # Private AISStream key for the server-side websocket. Do not prefix with VITE_. AISSTREAM_API_KEY= # Optional AISStream subscription filters. # AISSTREAM_BOUNDING_BOXES=[[[-90,-180],[90,180]]] # AISSTREAM_MESSAGE_TYPES=PositionReport,StandardClassBPositionReport,ExtendedClassBPositionReport,ShipStaticData,StaticDataReport # Feed watchdog: silence (ms) after which the feed is REPORTED stale; the socket # is recycled at 2.5x that. Default 120000. The watchdog self-arms only for the # default worldwide subscription — a narrow filter above can be legitimately # quiet, so set this explicitly (sized for your filter) to opt back in. # 0 disables the silence watch entirely. # AISSTREAM_SILENCE_TIMEOUT_MS=120000 # Optional: local ADS-B decoder feeds for the Local ADS-B layer. SERVER-SIDE # ONLY. Comma-separated band=url entries; band is 1090 or 978 (UAT). Each host # must be loopback, a private (RFC1918) address, localhost or a *.local name; # the path must end in aircraft.json. Invalid entries are logged and skipped. # See docs/LOCAL-RECEIVERS.md. # LOCAL_RECEIVER_FEEDS=1090=http://localhost:8080/data/aircraft.json,978=http://localhost:8978/data/aircraft.json # Optional: TomTom live traffic flow (BYOK freemium — the free tier is 200,000 # Traffic Flow & Incidents tile requests per MONTH, not per day; no credit card # required. Get a key at https://my.tomtom.com/keys). SERVER-SIDE ONLY: # the browser fetches same-origin /api/tomtom/* and never sees the key. # Keyless fallback: the traffic layer runs its built-in simulation (white dots, # hardcoded per-road-class speeds) — no key required for the layer to work. # TOMTOM_API_KEY= # Optional soft cap on upstream tile fetches per UTC day (default 6000) — a # configurable application safety ceiling, not a guarantee of staying within # TomTom's free allowance (currently 200K tile requests/month: # https://docs.tomtom.com/pricing/). 6000/day keeps a 31-day month at 186K, # inside that allowance. Over the cap the proxy serves cached/stale tiles # instead of hitting upstream. # TOMTOM_DAILY_TILE_BUDGET=6000 # Optional: CCTV layer tuning (advanced). Defaults are sensible — leave unset # unless you're customizing the camera source pack. # CCTV_SOURCES_FILE=config/cctv_sources.austin.json # path to a source-pack JSON # CCTV_SOURCES_JSON= # inline source-pack JSON (overrides file) # CCTV_AUSTIN_ROWS_URL= # override the Austin Open Data catalog URL # CCTV_AUSTIN_MAX_SOURCES=250 # cap cameras loaded from the Austin catalog # CCTV_CALTRANS_DISTRICTS=4,7,11,3 # Caltrans districts; empty string disables Caltrans # CCTV_CALTRANS_MAX_SOURCES=300 # cap cameras loaded from Caltrans # CCTV_TFL_ENABLED=1 # set 0 to disable Transport for London JamCams # CCTV_TFL_MAX_SOURCES=250 # cap cameras loaded from TfL # CCTV_ONTARIO_ENABLED=1 # set 0 to disable Ontario 511 cameras # CCTV_ONTARIO_MAX_SOURCES=1000 # cap cameras loaded from Ontario 511 # CCTV_FINTRAFFIC_ENABLED=1 # set 0 to drop the Fintraffic (Finland) pack # CCTV_FINTRAFFIC_MAX_SOURCES=300 # cap cameras loaded from the Fintraffic catalog # CCTV_DRIVEBC_MAX_SOURCES=250 # cap DriveBC (British Columbia) cameras, nearest Vancouver/Victoria first # CCTV_DRIVEBC_ENABLED=1 # 0 turns off the DriveBC camera pack # CCTV_TXDOT_ENABLED=1 # set 0 to disable the TxDOT (Texas) pack # CCTV_TXDOT_DISTRICTS=AUS,SAT # TxDOT district codes (25 exist); empty disables the pack # CCTV_TXDOT_MAX_SOURCES=500 # cap cameras loaded from the TxDOT catalogs # CCTV_TALLINN_ENABLED=1 # set 0 to disable the Tallinn (Estonia) intersection cameras # CCTV_TALLINN_MAX_SOURCES=255 # cap cameras loaded from the Tallinn catalog # CCTV_TARKTEE_ENABLED=1 # set 0 to disable the Tarktee (Estonia) road-weather cameras # CCTV_TARKTEE_MAX_SOURCES=179 # cap cameras loaded from Tarktee # CCTV_WARENDORF_ENABLED=1 # set 0 to disable the Warendorf (Germany) municipal webcams # CCTV_NSW_ENABLED=1 # set 0 to disable the Live Traffic NSW (Australia) cameras # CCTV_NSW_MAX_SOURCES=250 # cap cameras loaded from Live Traffic NSW # CCTV_CALGARY_ENABLED=1 # set 0 to disable the Open Calgary (Alberta) cameras # CCTV_CALGARY_MAX_SOURCES=220 # cap cameras loaded from the Open Calgary catalog # CCTV_CALGARY_ROWS_URL= # override the Open Calgary catalog URL # CCTV_VEGVESEN_ENABLED=1 # set 0 to disable the Statens vegvesen (Norway) road cameras # CCTV_VEGVESEN_MAX_SOURCES=900 # cap cameras loaded from Statens vegvesen (default loads all ~850 live; max 1000) # CCTV_VEGVESEN_VIDEO=1 # set 0 to show stills instead of live HLS for Statens vegvesen video cameras # CCTV_VEGVESEN_URL= # override the Statens vegvesen catalog URL # CCTV_MAX_SOURCES=4000 # catalog-wide ceiling, shared round-robin across packs # CCTV_PREFER_AUSTIN=1 # prioritize Austin cameras in the registry # CCTV_FORCE_AUSTIN= # force live default sources when a source-pack JSON is configured # Optional: comma-separated Overpass URLs; use only instances you run or pay for. OVERPASS_UPSTREAMS= # Optional: who may frame the app in embed mode (?embed=1), as CSP # frame-ancestors sources such as "https://example.com", or * for any page. # Unset, no page may frame it. Other pages of the app can never be framed. # GEV_EMBED_FRAME_ANCESTORS=