docs: add SvelteKit runtime env injection guidelines and troubleshooting

- Add comprehensive guide in .claude/guidelines/sveltekit-web.md about
  build-time vs runtime environment variables for Docker deployments
- Document the correct hooks.server.ts + window injection pattern
- Add Problem 9 to TROUBLESHOOTING.md for hardcoded localhost URLs
- List which apps are fixed vs need fixing
- Add checklist for fixing new apps

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Wuesteon 2025-12-08 20:54:13 +01:00
parent 3d717eb16a
commit 7f7b8b6db0
2 changed files with 223 additions and 4 deletions

View file

@ -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 = `<script>
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${PUBLIC_MANA_CORE_AUTH_URL_CLIENT}";
window.__PUBLIC_BACKEND_URL__ = "${PUBLIC_BACKEND_URL_CLIENT}";
</script>`;
return html.replace('<head>', `<head>${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

View file

@ -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)