Weather API Proxy Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Move the OpenWeather API call from the client-side app to the Cloudflare Worker backend, removing the exposed API key.
Architecture: Add a /weather endpoint to the existing Cloudflare Worker that proxies requests to OpenWeather. The frontend sends lat/lon/units as query params; the Worker attaches the secret API key and forwards to OpenWeather, returning a normalized response.
Tech Stack: Cloudflare Workers (TypeScript), Astro frontend
Task 1: Add OPENWEATHER_KEY to Worker types and config
Files:
- Modify:
api/src/types.ts:210-216(Env interface) - Modify:
api/.dev.vars.example
Step 1: Add OPENWEATHER_KEY to the Env interface
In api/src/types.ts, add OPENWEATHER_KEY?: string; to the Env interface:
export interface Env { CHAT_ROOM: DurableObjectNamespace; ADMIN_SECRET: string; ALLOWED_ORIGIN: string; OPENAI_API_KEY?: string; IPREGISTRY_KEY?: string; OPENWEATHER_KEY?: string;}Step 2: Add OPENWEATHER_KEY to .dev.vars.example
Append to api/.dev.vars.example:
# OpenWeather API key for weather data (used by /weather endpoint).# Get one at https://openweathermap.org/apiOPENWEATHER_KEY=Step 3: Commit
git add api/src/types.ts api/.dev.vars.examplegit commit -m "feat(api): add OPENWEATHER_KEY to Worker env"Task 2: Add handleWeather() handler and route
Files:
- Modify:
api/src/api.ts(add handler afterhandleLocation) - Modify:
api/src/index.ts(add route)
Step 1: Add handleWeather to api/src/api.ts
Add this function after handleLocation:
export async function handleWeather(env: Env, request: Request): Promise<Response> { const headers = _corsHeaders(env, request);
if (!env.OPENWEATHER_KEY) { return _jsonResponse({ error: "Weather service not configured" }, 503, headers); }
const url = new URL(request.url); const lat = url.searchParams.get("lat"); const lon = url.searchParams.get("lon"); const units = url.searchParams.get("units") || "metric";
if (!lat || !lon) { return _jsonResponse({ error: "lat and lon query params required" }, 400, headers); }
try { const res = await fetch( `https://api.openweathermap.org/data/2.5/weather?units=${encodeURIComponent(units)}&lat=${encodeURIComponent(lat)}&lon=${encodeURIComponent(lon)}&appid=${env.OPENWEATHER_KEY}`, ); if (!res.ok) { const body = await res.text(); console.error(`[weather] OpenWeather error ${res.status}: ${body}`); return _jsonResponse({ error: "Upstream weather service error" }, 502, headers); }
const data = (await res.json()) as { weather?: Array<{ main?: string; description?: string; icon?: string }>; main?: { temp?: number }; sys?: { sunrise?: number; sunset?: number }; };
return _jsonResponse( { main: data.weather?.[0]?.main ?? null, description: data.weather?.[0]?.description ?? null, icon: data.weather?.[0]?.icon ?? null, temp: data.main?.temp ?? null, sunrise: data.sys?.sunrise ?? null, sunset: data.sys?.sunset ?? null, }, 200, headers, ); } catch { return _jsonResponse({ error: "Weather lookup failed" }, 502, headers); }}Step 2: Add route in api/src/index.ts
Add import of handleWeather and add the route before the health check:
import { handleCors, handleWebSocket, handleAuth, handleConfig, handleHealthCheck, handleLocation, handleWeather } from "./api";Add this clause after the /location route:
} else if (url.pathname === "/weather") { return handleWeather(env, request);Step 3: Run type check
Run: just api::typecheck
Expected: No errors
Step 4: Commit
git add api/src/api.ts api/src/index.tsgit commit -m "feat(api): add /weather endpoint proxying OpenWeather"Task 3: Update frontend fetchWeather() to call the Worker
Files:
- Modify:
app/src/scripts/live-window/api.ts:74-104
Step 1: Change fetchWeather signature and implementation
Replace the fetchWeather function. Remove the owKey parameter, add apiUrl parameter. Call the Worker’s /weather endpoint instead of OpenWeather directly:
export async function fetchWeather( apiUrl: string, state: StoreState, units: string,): Promise<{ state: StoreState; changed: boolean }> { const { lat, lng } = state.location; if (lat == null || lng == null) return { state, changed: false };
try { const res = await fetch( `${apiUrl}/weather?units=${units}&lat=${lat}&lon=${lng}`, ); if (!res.ok) return { state, changed: false }; const data = await res.json();
const newState: StoreState = { ...state, weather: { current: { main: data.main, description: data.description, icon: data.icon, temp: data.temp }, sunrise: data.sunrise * 1000, sunset: data.sunset * 1000, units, lastFetched: Date.now(), }, };
return { state: newState, changed: true }; } catch { return { state, changed: false }; }}Step 2: Commit
git add app/src/scripts/live-window/api.tsgit commit -m "feat(app): fetchWeather calls Worker instead of OpenWeather directly"Task 4: Update LiveWindow.ts to remove openweather-key attribute
Files:
- Modify:
app/src/scripts/live-window/LiveWindow.ts
Step 1: Remove "openweather-key" from observedAttributes
Remove "openweather-key", from the static observedAttributes array (line 16).
Step 2: Update attributeChangedCallback
Remove the condition at lines 96-98 that checks for openweather-key:
if (this.getAttribute("openweather-key") && this.getAttribute("api-url") && !this.weatherInterval) { this.startWeatherPolling(); }Replace with:
if (this.getAttribute("api-url") && !this.weatherInterval) { this.startWeatherPolling(); }Step 3: Update startUpdates
Replace the condition at lines 196-203:
const hasExplicitCoords = this.getAttribute("latitude") != null && this.getAttribute("longitude") != null;
if ( (hasExplicitCoords && this.getAttribute("openweather-key")) || (this.getAttribute("openweather-key") && this.getAttribute("api-url")) ) { this.startWeatherPolling(); }With:
const apiUrl = this.getAttribute("api-url"); const hasExplicitCoords = this.getAttribute("latitude") != null && this.getAttribute("longitude") != null;
if (apiUrl && (hasExplicitCoords || true)) { this.startWeatherPolling(); }Wait — simplify. The condition was: need an OW key AND either explicit coords or api-url. Now OW key is on the Worker, so the condition is just: need api-url.
if (this.getAttribute("api-url")) { this.startWeatherPolling(); }Step 4: Update doFetchWeather
Replace the method. Key changes:
- Remove
owKeycheck — no longer needed - Always require
apiUrl - Pass
apiUrltofetchWeatherinstead ofowKey - For explicit coords, still need
apiUrlfor the weather call
private async doFetchWeather(): Promise<void> { const apiUrl = this.getAttribute("api-url"); if (!apiUrl) return;
const explicitLat = this.getAttribute("latitude"); const explicitLng = this.getAttribute("longitude"); const hasExplicitCoords = explicitLat != null && explicitLng != null;
if (hasExplicitCoords) { this.state.store = { ...this.state.store, location: { lat: parseFloat(explicitLat), lng: parseFloat(explicitLng), country: null, name: null, timezone: null, lastFetched: Date.now(), }, }; } else { if (!shouldFetchWeather(this.state.store, this.state.attrs.resolvedUnits)) { this.updateAll(); return; }
this.state.store = await fetchLocation(apiUrl, this.state.store); saveState(this.state); }
this.refreshAttrs(); const units = this.state.attrs.resolvedUnits;
if (!shouldFetchWeather(this.state.store, units) && !hasExplicitCoords) { this.updateAll(); return; }
const result = await fetchWeather(apiUrl, this.state.store, units); this.state.store = result.state;
if (!hasExplicitCoords) { saveState(this.state); }
if (result.changed) { this.updateAll(); this.dispatchEvent( new CustomEvent("live-window:weather-update", { detail: { weather: this.state.store.weather }, }), ); } }Step 5: Commit
git add app/src/scripts/live-window/LiveWindow.tsgit commit -m "feat(app): remove openweather-key attribute, use api-url for weather"Task 5: Remove PUBLIC_OPENWEATHER_KEY from pages, env, and CI
Files:
- Modify:
app/src/pages/index.astro - Modify:
app/src/pages/live-window-test.astro - Modify:
app/.env.example - Modify:
.github/workflows/deploy.yml
Step 1: Update index.astro
Remove line 8: const openweatherKey = import.meta.env.PUBLIC_OPENWEATHER_KEY || "";
Remove openweather-key={openweatherKey} from the <live-window> tag on line 25.
Step 2: Update live-window-test.astro
Remove line 4: const openweatherKey = import.meta.env.PUBLIC_OPENWEATHER_KEY || "";
Remove openweather-key={openweatherKey} from the <live-window> tag on line 29.
Add api-url attribute so the test page can fetch weather through the Worker. Add this to the frontmatter:
const apiBaseUrl = import.meta.env.PUBLIC_API_BASE_URL || "";Add api-url={apiBaseUrl} to the <live-window> tag.
Step 3: Update app/.env.example
Remove the three lines about OpenWeather (lines 11-13):
# OpenWeather API key for the live window weather display.# Get one at https://openweathermap.org/apiPUBLIC_OPENWEATHER_KEY=Step 4: Update .github/workflows/deploy.yml
Remove line 108: PUBLIC_OPENWEATHER_KEY: ${{ secrets.PUBLIC_OPENWEATHER_KEY }}
Step 5: Run type check and build
Run: just app::typecheck
Expected: No errors
Run: just app::build
Expected: Build succeeds
Step 6: Commit
git add app/src/pages/index.astro app/src/pages/live-window-test.astro app/.env.example .github/workflows/deploy.ymlgit commit -m "feat(app): remove PUBLIC_OPENWEATHER_KEY from client-side code and CI"Task 6: Update README
Files:
- Modify:
app/src/scripts/live-window/README.md
Step 1: Update attribute table
Remove the openweather-key row. Update the description of api-url to mention it’s used for both geolocation and weather.
Step 2: Update data flow description
Update the sections that describe how weather fetching works to reflect the new proxy pattern.
Step 3: Commit
git add app/src/scripts/live-window/README.mdgit commit -m "docs: update live-window README for weather API proxy"