diff --git a/docs/superpowers/specs/2026-04-02-pwa-design.md b/docs/superpowers/specs/2026-04-02-pwa-design.md
new file mode 100644
index 0000000..4577d22
--- /dev/null
+++ b/docs/superpowers/specs/2026-04-02-pwa-design.md
@@ -0,0 +1,180 @@
+# PWA Design Spec: apps/main
+
+**Date**: 2026-04-02
+**Goal**: Make apps/main installable as a native-feeling PWA with fast repeat loads, focused on mobile experience polish. No offline data, no push notifications.
+**Approach**: `vite-plugin-pwa` (Workbox-based) integrated into the existing Vite build.
+
+---
+
+## 1. Web App Manifest & Icons
+
+### Manifest
+
+Generated by `vite-plugin-pwa` from config in `vite.config.ts`. Key properties:
+
+| Property | Value |
+|----------|-------|
+| `name` | XTablo |
+| `short_name` | XTablo |
+| `start_url` | `/` |
+| `display` | `standalone` |
+| `theme_color` | Matched to app header (from Tailwind config) |
+| `background_color` | Matched to app background |
+| `orientation` | `any` |
+| `scope` | `/` |
+
+### Icons
+
+Generated from existing `icon.jpg` (production) and `staging_icon.jpg` (staging) using a one-time script (e.g., `sharp`). Output committed to `apps/main/public/pwa-icons/`.
+
+Required sizes:
+- `16x16`, `32x32` — favicons
+- `180x180` — Apple touch icon
+- `192x192` — Android standard
+- `512x512` — Android standard
+- `512x512 maskable` — Android adaptive icon
+
+### HTML Meta Tags
+
+Added to `apps/main/index.html`:
+
+```html
+
+
+
+
+```
+
+Note: The manifest `` is injected automatically by `vite-plugin-pwa`.
+
+---
+
+## 2. Service Worker & Caching Strategy
+
+### Plugin Configuration
+
+`vite-plugin-pwa` in `vite.config.ts` with:
+- `registerType: 'autoUpdate'` — silent background updates, no user prompt
+- `workbox.globPatterns` — precache all build outputs (JS, CSS, fonts, images)
+
+### Caching Rules
+
+| Resource | Strategy | Notes |
+|----------|----------|-------|
+| App shell (HTML, JS, CSS) | Precache | Auto-generated by Workbox from build manifest |
+| Static assets (icons, logos) | Precache | Included in build output |
+| API calls (`/api/v1/*`) | Network only | Not cached; errors handled by React Query |
+| Supabase calls | Network only | Not cached; errors handled by React Query |
+
+### SW Registration
+
+In `main.tsx`, using `registerSW` from `virtual:pwa-register`. Handles registration lifecycle and auto-update.
+
+### Update Flow
+
+1. User opens/refreshes the app
+2. SW detects new precache manifest in background
+3. New assets downloaded silently
+4. New version activates on next navigation/refresh
+
+No "update available" toast — the app always requires connectivity, so silent updates are sufficient.
+
+---
+
+## 3. Install Prompt UX
+
+### Custom Install Prompt
+
+A React hook `useInstallPrompt` that:
+- Listens for the `beforeinstallprompt` event
+- Stores the deferred prompt event
+- Exposes `canInstall: boolean` and `promptInstall(): void`
+
+### UI
+
+- Dismissible banner shown to authenticated users on the dashboard
+- Dismissal persisted in `localStorage` (key: `pwa-install-dismissed`)
+- Hidden when app is already running in standalone mode (detected via `window.matchMedia('(display-mode: standalone)')`)
+
+### iOS Fallback
+
+Safari does not support `beforeinstallprompt`. For iOS Safari users:
+- Detect via user agent
+- Show manual instruction: "Tap Share > Add to Home Screen"
+- Same dismissal logic applies
+
+---
+
+## 4. Mobile Polish & Standalone Experience
+
+### Viewport & Safe Areas
+
+Update viewport meta tag:
+```html
+
+```
+
+Add CSS safe area padding on the app shell:
+```css
+padding-top: env(safe-area-inset-top);
+padding-bottom: env(safe-area-inset-bottom);
+padding-left: env(safe-area-inset-left);
+padding-right: env(safe-area-inset-right);
+```
+
+### Splash Screens
+
+Simple approach — no custom splash images. Use `theme_color` + `background_color` in the manifest for Android. iOS uses `apple-mobile-web-app-status-bar-style` for a clean launch.
+
+### Status Bar
+
+- Android: Controlled by `theme_color` in manifest
+- iOS: `apple-mobile-web-app-status-bar-style: default`
+
+### Navigation
+
+Standalone mode has no browser back button. The existing React Router navigation with in-app back buttons handles this. Verify no dead-end flows exist.
+
+---
+
+## 5. Environment & Deployment
+
+### Environment Differences
+
+| Aspect | Staging | Production |
+|--------|---------|------------|
+| Icon source | `staging_icon.jpg` | `icon.jpg` |
+| Manifest name | XTablo (Staging) | XTablo |
+| All other config | Same | Same |
+
+### Cloudflare Workers
+
+No changes to `wrangler.toml`. The SW file and manifest are static assets in `dist/`, served by Cloudflare like any other file. SPA routing (`not_found_handling = "single-page-application"`) is unaffected.
+
+### Build Pipeline
+
+- `vite-plugin-pwa` runs as part of normal `pnpm build` — no separate step
+- Icon generation is a one-time script, output committed to repo
+
+### Local Testing
+
+- `pnpm dev`: SW disabled (standard behavior for vite-plugin-pwa)
+- `pnpm build && pnpm preview`: Full PWA experience with active SW
+
+### Impact
+
+Fully additive. Users who don't install the PWA see no changes. The app works identically in a regular browser tab.
+
+---
+
+## Out of Scope
+
+- Offline data access (all data requires connectivity)
+- Push notifications (deferred to future work)
+- Background sync
+- Custom splash screen images (using color-based approach instead)
+
+## Dependencies
+
+- `vite-plugin-pwa` — Vite PWA integration
+- `sharp` (dev dependency) — icon generation script