# SvelteKit SSR Best Practices Guide

This guide documents SSR-safe patterns and anti-patterns for InstaRecipe development. All developers should follow these guidelines to prevent SSR errors.

## Table of Contents

- [Core Principle](#core-principle)
- [Browser API Detection](#browser-api-detection)
- [Lifecycle Hooks](#lifecycle-hooks)
- [Runes and Reactivity](#runes-and-reactivity)
- [Common Gotchas](#common-gotchas)
- [Good Examples from Codebase](#good-examples-from-codebase)
- [Anti-Patterns to Avoid](#anti-patterns-to-avoid)

---

## Core Principle

**SvelteKit renders components on both the server and client.** Any browser-only APIs must be guarded or used in browser-only contexts.

### Browser-Only APIs (Require Guards)

- `window.*`
- `document.*`
- `localStorage`, `sessionStorage`
- `navigator.*`
- `EventSource`, `WebSocket`
- `location.*`
- `fetch` in certain contexts (use SvelteKit's built-in fetch in load functions)

---

## Browser API Detection

### Pattern: Use `browser` from `$app/environment`

```typescript
import { browser } from '$app/environment';

if (browser) {
	// Safe: only runs in browser
	const data = localStorage.getItem('key');
}
```

**Why it works:** `browser` is `true` only when running in the browser, `false` during SSR.

### Example: EventSource Connection

```svelte
<script lang="ts">
  import { browser } from '$app/environment';
  import { onMount } from 'svelte';

  let eventSource = $state<EventSource | null>(null);

  function startSSEConnection() {
    if (!browser) return; // ✅ Guard
    eventSource = new EventSource('/api/stream');
  }

  onMount(() => {
    if (browser) { // ✅ Explicit guard
      startSSEConnection();
    }
  });
</script>
```

---

## Lifecycle Hooks

### `onMount` - Browser-Only Lifecycle

**Use `onMount` for:**

- Browser API initialization
- Timer setup (`setInterval`, `setTimeout`)
- Event listener registration
- Any browser-only side effects

```typescript
import { onMount } from 'svelte';

onMount(() => {
	// ✅ Only runs in browser (built-in SSR guard)
	const interval = setInterval(() => {
		// Polling logic
	}, 1000);

	return () => clearInterval(interval); // Cleanup
});
```

### `onDestroy` - Cleanup

```typescript
import { onDestroy } from 'svelte';

onDestroy(() => {
	// ✅ Safe for cleanup
	eventSource?.close();
});
```

---

## Runes and Reactivity

### `$state` - Reactive State

```typescript
let count = $state(0); // ✅ SSR-safe
let user = $state<User | null>(null); // ✅ SSR-safe with null default

// ❌ DON'T: Initialize with browser APIs
let stored = $state(localStorage.getItem('key')); // SSR crash!

// ✅ DO: Load in onMount
let stored = $state<string | null>(null);
onMount(() => {
	stored = localStorage.getItem('key');
});
```

### `$derived` - Computed Values

```typescript
// ✅ GOOD: Pure computation
let doubled = $derived(count * 2);
let fullName = $derived(`${firstName} ${lastName}`);

// ❌ BAD: Side effects or browser APIs
let data = $derived(localStorage.getItem('key')); // SSR crash!
let userAgent = $derived(navigator.userAgent); // SSR crash!
```

**Rule:** `$derived` must be pure (no side effects, no browser APIs).

### `$effect` - Reactive Side Effects

**Critical:** `$effect` runs during **both SSR and hydration**. Always guard browser APIs!

```typescript
// ❌ BAD: No browser guard
$effect(() => {
	setInterval(() => checkHealth(), 1000); // SSR crash!
});

// ✅ GOOD: With browser guard
$effect(() => {
	if (!browser) return;
	const interval = setInterval(() => checkHealth(), 1000);
	return () => clearInterval(interval);
});

// ✅ BETTER: Use onMount for initialization instead
onMount(() => {
	const interval = setInterval(() => checkHealth(), 1000);
	return () => clearInterval(interval);
});
```

**When to use `$effect`:**

- Synchronizing derived state
- DOM manipulation (with browser guard)
- Reactive cleanup

**When NOT to use `$effect`:**

- Initialization (use `onMount`)
- API calls on mount (use `onMount`)
- Timer setup (use `onMount`)

---

## Common Gotchas

### 1. Static Constants from Browser APIs

```typescript
// ❌ BAD: Static properties don't exist during SSR
if (eventSource?.readyState === EventSource.OPEN) // SSR crash!
if (ws.readyState === WebSocket.OPEN) // SSR crash!

// ✅ GOOD: Use numeric constants
if (eventSource?.readyState === 1) // EventSource.OPEN = 1
if (ws.readyState === 1) // WebSocket.OPEN = 1

// ✅ GOOD: Guard the check
if (browser && eventSource?.readyState === EventSource.OPEN)
```

**EventSource States:**

- `EventSource.CONNECTING = 0`
- `EventSource.OPEN = 1`
- `EventSource.CLOSED = 2`

**WebSocket States:**

- `WebSocket.CONNECTING = 0`
- `WebSocket.OPEN = 1`
- `WebSocket.CLOSING = 2`
- `WebSocket.CLOSED = 3`

### 2. Event Handlers (Already Safe)

Event handlers (`onclick`, `onsubmit`, etc.) only run in the browser, so no guard needed:

```svelte
<button onclick={() => {
  // ✅ Safe: event handlers only run in browser
  localStorage.setItem('key', 'value');
}}>
  Click me
</button>
```

### 3. Timers in Components

```typescript
// ❌ BAD: At module level
const interval = setInterval(() => {}, 1000); // SSR crash!

// ✅ GOOD: In onMount
onMount(() => {
	const interval = setInterval(() => {}, 1000);
	return () => clearInterval(interval);
});
```

### 4. Conditional Rendering Based on `browser`

```svelte
<!-- ⚠️ AVOID: Causes hydration mismatch -->
{#if browser}
  <ClientOnlyComponent />
{/if}

<!-- ✅ BETTER: Initialize in onMount with flag -->
<script>
  let mounted = $state(false);
  onMount(() => {
    mounted = true;
  });
</script>

{#if mounted}
  <ClientOnlyComponent />
{/if}
```

**Why:** The server renders one thing, the client renders another, causing hydration warnings.

---

## Good Examples from Codebase

### Example 1: PushNotificationManager (Excellent)

[src/lib/client/PushNotificationManager.ts](../src/lib/client/PushNotificationManager.ts)

```typescript
import { browser } from '$app/environment';

export class PushNotificationManager {
	private static instance: PushNotificationManager | null = null;

	static getInstance() {
		if (!browser) return null; // ✅ Early return for SSR
		// ... rest of implementation
	}

	private loadStoredSubscription() {
		if (!browser) return null; // ✅ Guard localStorage
		const stored = localStorage.getItem('pushSubscription');
		return stored ? JSON.parse(stored) : null;
	}
}
```

**Why it's good:**

- Guards all browser API access
- Early returns prevent unnecessary code execution during SSR
- Defensive programming with null checks

### Example 2: Queue Dashboard (Fixed)

[src/routes/+page.svelte](../src/routes/+page.svelte)

```svelte
<script lang="ts">
  import { browser } from '$app/environment';
  import { onMount } from 'svelte';

  let eventSource = $state<EventSource | null>(null);

  onMount(async () => {
    await loadQueueItems();
    if (browser) { // ✅ Guard
      startSSEConnection();
    }
  });

  function startSSEConnection() {
    if (!browser) return; // ✅ Double guard for safety
    eventSource = new EventSource('/api/queue/stream');
    // ...
  }
</script>

<!-- Template: Use numeric constants -->
<div class="indicator {eventSource?.readyState === 1 ? 'online' : 'offline'}"></div>
```

### Example 3: LLM Health Indicator (Fixed)

[src/routes/share/components/LlmHealthIndicator.svelte](../src/routes/share/components/LlmHealthIndicator.svelte)

```svelte
<script lang="ts">
  import { onMount } from 'svelte';

  onMount(() => {
    // ✅ onMount only runs in browser
    checkHealth(); // Initial check
    const interval = setInterval(checkHealth, pollInterval);
    return () => clearInterval(interval);
  });
</script>
```

**Why it's good:**

- Uses `onMount` instead of `$effect` for initialization
- Timer setup in browser-only context
- Proper cleanup with return function

---

## Anti-Patterns to Avoid

### ❌ Anti-Pattern 1: Browser APIs in `$derived`

```typescript
// ❌ DON'T
let theme = $derived(localStorage.getItem('theme'));

// ✅ DO
let theme = $state<string | null>(null);
onMount(() => {
	theme = localStorage.getItem('theme');
});
```

### ❌ Anti-Pattern 2: Side Effects in `$effect` Without Guards

```typescript
// ❌ DON'T
$effect(() => {
	// Runs during SSR!
	fetch('/api/data');
});

// ✅ DO: Guard browser-specific side effects
$effect(() => {
	if (!browser) return;
	fetch('/api/data');
});

// ✅ BETTER: Use onMount for initialization
onMount(() => {
	fetch('/api/data');
});
```

### ❌ Anti-Pattern 3: Static Browser Constants

```typescript
// ❌ DON'T
if (ws.readyState === WebSocket.OPEN)

// ✅ DO
if (ws.readyState === 1) // WebSocket.OPEN = 1
```

### ❌ Anti-Pattern 4: Timers at Module Level

```typescript
// ❌ DON'T
const interval = setInterval(() => {}, 1000);

// ✅ DO
onMount(() => {
	const interval = setInterval(() => {}, 1000);
	return () => clearInterval(interval);
});
```

---

## Testing for SSR Safety

### 1. Build and Preview

```bash
npm run build
npm run preview
```

Watch server console for errors during build and preview.

### 2. Check for Hydration Warnings

Open browser DevTools console and look for:

- "Hydration failed"
- "The server response doesn't match the client content"

### 3. Search for Unguarded APIs

```bash
# Search for potential SSR issues
grep -r "window\." src/routes --include="*.svelte"
grep -r "document\." src/routes --include="*.svelte"
grep -r "localStorage" src/routes --include="*.svelte"
grep -r "navigator\." src/routes --include="*.svelte"
```

Then verify each usage is either:

- In an event handler (safe)
- In `onMount` (safe)
- Guarded with `if (browser)` (safe)

---

## Quick Reference Checklist

When writing component code, ask:

- [ ] Am I using any browser APIs? (`window`, `document`, `localStorage`, etc.)
  - **Yes:** Add `browser` guard or use `onMount`
  - **No:** Proceed normally

- [ ] Am I using `$effect`?
  - **For synchronization:** OK, but guard browser APIs
  - **For initialization:** Use `onMount` instead

- [ ] Am I using static properties from browser APIs?
  - **Yes:** Use numeric constants or add `browser` guard
  - **No:** You're good

- [ ] Does my code need cleanup?
  - **Yes:** Return cleanup function from `onMount` or `$effect`
  - **No:** You're good

---

## Further Reading

- [SvelteKit Documentation](https://kit.svelte.dev/docs)
- [Svelte Runes Documentation](https://svelte.dev/docs/svelte/$state)
- [MDN: EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource)
- [MDN: Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)

---

**Document Version:** 1.0  
**Last Updated:** December 22, 2025  
**Related Outcome:** [FixEventSourceSSR](./outcomes/FixEventSourceSSR.md)
