# 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
<meta name="theme-color" content="#...">
<link rel="apple-touch-icon" href="/pwa-icons/apple-touch-icon-180x180.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
```

Note: The manifest `<link>` 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
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
```

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
