diff --git a/.claude/guidelines/sveltekit-web.md b/.claude/guidelines/sveltekit-web.md index a307c9352..4a69dd273 100644 --- a/.claude/guidelines/sveltekit-web.md +++ b/.claude/guidelines/sveltekit-web.md @@ -698,15 +698,138 @@ export default { ## Environment Variables -```typescript -// Access in .svelte or .ts files -import { PUBLIC_BACKEND_URL, PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public'; +### Build-Time vs Runtime Variables -// .env file +SvelteKit has **two types** of environment variables: + +1. **Build-time** (`$env/static/public`) - Baked into the bundle at build time +2. **Runtime** (`process.env`) - Available at runtime in server code + +**CRITICAL**: For Docker deployments, browser-facing URLs must use **runtime injection** because: + +- Docker images are built once but deployed to different environments (staging, production) +- Build-time variables would require rebuilding the image for each environment +- The browser cannot access `process.env` - it needs values injected into the HTML + +### ❌ WRONG - Hardcoded or Build-Time URLs + +```typescript +// ❌ BAD - Hardcoded URL (won't work in Docker) +const MANA_AUTH_URL = 'http://localhost:3001'; + +// ❌ BAD - Build-time variable (works locally, breaks in Docker) +import { PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public'; +const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; + +// ❌ BAD - import.meta.env is also build-time +const MANA_AUTH_URL = import.meta.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; +``` + +### ✅ CORRECT - Runtime Injection Pattern + +**Step 1: Create `hooks.server.ts`** to inject env vars into HTML: + +```typescript +// src/hooks.server.ts +import type { Handle } from '@sveltejs/kit'; + +// Get client-side URLs from Docker runtime environment +const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = + process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; +const PUBLIC_BACKEND_URL_CLIENT = + process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; + +export const handle: Handle = async ({ event, resolve }) => { + return resolve(event, { + transformPageChunk: ({ html }) => { + // Inject runtime environment variables into the HTML + const envScript = ``; + return html.replace('', `${envScript}`); + }, + }); +}; +``` + +**Step 2: Read from `window` in client code:** + +```typescript +// src/lib/stores/auth.svelte.ts +import { browser } from '$app/environment'; + +function getAuthUrl(): string { + if (browser && typeof window !== 'undefined') { + // Client-side: use injected window variable + const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) + .__PUBLIC_MANA_CORE_AUTH_URL__; + return injectedUrl || 'http://localhost:3001'; + } + // Server-side (SSR): use Docker internal URL + return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; +} + +// Use in auth service initialization +const auth = initializeWebAuth({ baseUrl: getAuthUrl() }); +``` + +**Step 3: Set environment variables in `docker-compose.staging.yml`:** + +```yaml +services: + myapp-web: + environment: + # Server-side URLs (Docker internal network) + PUBLIC_BACKEND_URL: http://myapp-backend:3000 + PUBLIC_MANA_CORE_AUTH_URL: http://mana-core-auth:3001 + # Client-side URLs (browser access via public IP) + PUBLIC_BACKEND_URL_CLIENT: http://46.224.108.214:3000 + PUBLIC_MANA_CORE_AUTH_URL_CLIENT: http://46.224.108.214:3001 +``` + +### Why Two URLs? + +| Variable | Purpose | Example | +| ---------------------------------- | --------------------------------- | ---------------------------- | +| `PUBLIC_MANA_CORE_AUTH_URL` | Server-to-server (SSR, API calls) | `http://mana-core-auth:3001` | +| `PUBLIC_MANA_CORE_AUTH_URL_CLIENT` | Browser to server | `http://46.224.108.214:3001` | + +Docker containers can reach each other by service name (`mana-core-auth`), but browsers need the public IP/domain. + +### Apps Using This Pattern Correctly + +- ✅ `chat/apps/web` - Has `hooks.server.ts` with runtime injection +- ✅ `todo/apps/web` - Fixed +- ✅ `calendar/apps/web` - Fixed +- ✅ `clock/apps/web` - Fixed + +### Apps That Still Need Fixing + +- ❌ `contacts/apps/web` +- ❌ `manadeck/apps/web` +- ❌ `manacore/apps/web` +- ❌ `zitare/apps/web` +- ❌ `picture/apps/web` + +### Quick Checklist for New SvelteKit Apps + +- [ ] Create `src/hooks.server.ts` with env injection +- [ ] Update `auth.svelte.ts` to use `getAuthUrl()` pattern +- [ ] Update `user-settings.svelte.ts` to use `getAuthUrl()` pattern +- [ ] Update any feedback services to use runtime URL +- [ ] Add both `_CLIENT` and non-client env vars to `docker-compose.staging.yml` +- [ ] Never hardcode `localhost:3001` anywhere + +### Simple .env (for local development only) + +```env PUBLIC_BACKEND_URL=http://localhost:3016 PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001 ``` +These work locally because both the browser and server access `localhost`. + ## Anti-Patterns to Avoid ### Don't Use Old Svelte Syntax diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index db84b689c..a84dcb359 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -17,6 +17,7 @@ Common issues and solutions for the manacore-monorepo. - [CORS Blocking Cross-Origin Requests](#problem-6-cors-blocking-cross-origin-requests) - [Missing Database Schema](#problem-7-missing-database-schema) - [pnpm Symlinks Broken in Docker Container](#problem-8-pnpm-symlinks-broken-in-docker-container) + - [Hardcoded localhost URLs in SvelteKit Web Apps](#problem-9-hardcoded-localhost-urls-in-sveltekit-web-apps) --- @@ -1053,6 +1054,101 @@ CMD ["node", "build"] --- +### Problem 9: Hardcoded localhost URLs in SvelteKit Web Apps + +**Symptoms:** + +- Browser console shows: `POST http://localhost:3001/api/v1/auth/login net::ERR_CONNECTION_REFUSED` +- App works locally but auth fails on staging/production +- The `window.__PUBLIC_MANA_CORE_AUTH_URL__` may be set correctly, but code doesn't use it +- Looking at the source code reveals hardcoded URLs like `const MANA_AUTH_URL = 'http://localhost:3001'` + +**Root Cause:** + +Developers hardcode `localhost:3001` directly in TypeScript files instead of using the runtime injection pattern. This works locally but breaks in Docker deployments. + +**Common Locations of Hardcoded URLs:** + +```typescript +// ❌ These patterns are WRONG: + +// In auth.svelte.ts +const MANA_AUTH_URL = 'http://localhost:3001'; + +// In user-settings.svelte.ts +const MANA_AUTH_URL = 'http://localhost:3001'; + +// In feedback.ts or feedback page +apiUrl: 'http://localhost:3001', + +// Using build-time env vars (also wrong for Docker) +const MANA_AUTH_URL = import.meta.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; +``` + +**Solution:** + +1. **Create `hooks.server.ts`** if it doesn't exist (see Problem 5) +2. **Use `getAuthUrl()` pattern in all files:** + +```typescript +// ✅ CORRECT pattern +import { browser } from '$app/environment'; + +function getAuthUrl(): string { + if (browser && typeof window !== 'undefined') { + const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) + .__PUBLIC_MANA_CORE_AUTH_URL__; + return injectedUrl || 'http://localhost:3001'; + } + return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; +} + +// Use getAuthUrl() instead of hardcoded string +const auth = initializeWebAuth({ baseUrl: getAuthUrl() }); +``` + +**How to Find Hardcoded URLs:** + +```bash +# Search for hardcoded localhost:3001 in web apps +grep -r "localhost:3001" apps/*/apps/web/src --include="*.ts" --include="*.svelte" + +# Check for the correct pattern (window injection) +grep -r "__PUBLIC_MANA_CORE_AUTH_URL__" apps/*/apps/web/src +``` + +**Apps Status (as of 2024-12):** + +| App | Status | Files to Fix | +| ------------------- | ------------ | ---------------------------------------------------------------- | +| `chat/apps/web` | ✅ Fixed | - | +| `todo/apps/web` | ✅ Fixed | - | +| `calendar/apps/web` | ✅ Fixed | - | +| `clock/apps/web` | ✅ Fixed | - | +| `contacts/apps/web` | ❌ Needs Fix | auth.svelte.ts, user-settings.svelte.ts, feedback.ts | +| `manadeck/apps/web` | ❌ Needs Fix | user-settings.svelte.ts, feedback.ts | +| `manacore/apps/web` | ❌ Needs Fix | auth.svelte.ts, user-settings.svelte.ts, feedback.ts, credits.ts | +| `zitare/apps/web` | ❌ Needs Fix | auth.svelte.ts, user-settings.svelte.ts, feedback.ts | +| `picture/apps/web` | ❌ Needs Fix | user-settings.svelte.ts | + +**Complete Fix Checklist for Each App:** + +- [ ] Create/update `src/hooks.server.ts` with env injection +- [ ] Update `src/lib/stores/auth.svelte.ts` → use `getAuthUrl()` pattern +- [ ] Update `src/lib/stores/user-settings.svelte.ts` → use `getAuthUrl()` pattern +- [ ] Update any `feedback.ts` or feedback services → use `getAuthUrl()` pattern +- [ ] Update any other files with hardcoded URLs +- [ ] Add `PUBLIC_MANA_CORE_AUTH_URL_CLIENT` to `docker-compose.staging.yml` +- [ ] Test locally with `pnpm dev` +- [ ] Deploy and test on staging + +**See Also:** + +- [Problem 5: Client-Side Calling localhost Instead of Public IP](#problem-5-client-side-calling-localhost-instead-of-public-ip) +- [SvelteKit Web Guidelines - Environment Variables](/.claude/guidelines/sveltekit-web.md#environment-variables) + +--- + ## References - [CLAUDE.md - Turborepo Configuration](./CLAUDE.md#turborepo-configuration)