Files
JamBuddy/docs/pwa-firebase-plan.md

14 KiB
Raw Permalink Blame History

Plan: PWA + Firebase Hosting for JamBuddy

Status: proposed. Each phase is independently shippable; stop after any one.


0. Why this is low-risk

The Electron shell (electron/main.cjs, preload.cjs) does nothing but host a window — no IPC, no native APIs, sandbox:false only to give the renderer full Web Audio. All logic lives in the React renderer, which already runs browser-only via npm run dev. So the web build is the same vite build output the desktop installer already ships; we just add a manifest + service worker and serve dist/ over HTTPS. Firebase gives us HTTPS for free, which getUserMedia (the mic) requires — that's the only hard web constraint.

Nothing in the app makes network calls today, so there's no API surface to re-host and no secrets to manage.


Phase 1 — PWA scaffolding (no Firebase yet)

Goal: vite build produces an installable, offline-capable web app served from any static host.

1.1 Add vite-plugin-pwa

Only new dependency. Wraps Workbox under a Vite plugin — generates the service worker and injects the manifest. Cheaper than hand-rolling a SW and gets update flow for free.

// vite.config.js
import { VitePWA } from 'vite-plugin-pwa'

export default defineConfig({
  plugins: [
    react(),
    tailwindcss(),
    VitePWA({
      registerType: 'autoUpdate',
      includeAssets: ['favicon.ico', 'apple-touch-icon.png'],
      manifest: {
        name: 'JamBuddy — WhatTheFlat',
        short_name: 'JamBuddy',
        description: 'Real-time key and chord detection for live jams.',
        theme_color: '#0f0f0f',      // matches bg-surface token
        background_color: '#0f0f0f',
        display: 'standalone',
        orientation: 'any',
        start_url: '/',
        scope: '/',
        icons: [ /* see 1.3 */ ],
      },
      workbox: {
        globPatterns: ['**/*.{js,css,html,svg,png,ico,woff2}'],
        // Audio is captured live — nothing to cache from a remote. Precache the app shell only.
        navigateFallback: '/index.html',
      },
    }),
  ],
})

1.2 Create public/ directory

Vite serves public/ at root and copies it verbatim into dist/. We need it for:

  • favicon.ico, apple-touch-icon.png (180×180)
  • Any static PWA icons referenced by the manifest
  • (Optional) robots.txt — not needed for an app, skip.

The existing src/assets/whattheflat-logo.png is imported in App.jsx and bundled by Vite — leave that as-is; don't move it. The manifest icons must live in public/ because they're referenced by URL, not imported.

Derive all required sizes from assets/whattheflat-logo.png (one source image → PNG output). Required manifest entries:

icons: [
  { src: '/icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
  { src: '/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
  { src: '/icon-512-maskable.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
]

Plus apple-touch-icon.png (180×180) in public/ — iOS ignores the manifest and uses this <link>. The maskable variant needs safe-zone padding (~10%) so the logo isn't cropped by Android's circular mask.

One-liner with ImageMagick if available, else export manually from the source PNG:

magick assets/whattheflat-logo.png -resize 192x192 public/icon-192.png
magick assets/whattheflat-logo.png -resize 512x512 public/icon-512.png
magick assets/whattheflat-logo.png -resize 180x180 public/apple-touch-icon.png

1.4 Update index.html

Two problems with the current <head>:

  1. No PWA tags — add theme-color, manifest link, apple-touch-icon, apple-mobile-web-app-capable, description.
  2. CSP is hardcoded for devconnect-src 'self' http://localhost:5173 ws://localhost:5173 and script-src 'self' 'unsafe-eval'. The unsafe-eval and the localhost entries are Vite dev artifacts; production Vite doesn't need unsafe-eval. Keep a CSP (it's good hygiene and the app is genuinely offline) but drop the dev bits:
<meta name="theme-color" content="#0f0f0f" />
<meta name="description" content="Real-time key and chord detection for live jams." />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />

CSP (production):

default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
connect-src 'self';
media-src 'self' blob:;   <!-- the mic stream lives in a blob -->

blob: in media-src is required because getUserMedia streams are blob-backed and the analyser reads them. Verify nothing else regresses — vite-plugin-pwa registers the SW from a same-origin script, so 'self' covers it.

Move the dev-only CSP into a Vite conditional so dev still allows HMR/websocket:

// vite.config.js — inject dev CSP via plugin, or keep a separate dev index template.

Simplest: keep one CSP in index.html with the production values, and in dev let Vite's own injected tags coexist (HMR works over the same origin websocket; CSP connect-src 'self' already allows it). If dev breaks, add ws://localhost:5173 only in a dev-specific block.

1.5 Verify

  • npm run builddist/ contains manifest.webmanifest, registerSW.js, sw.js, and all public/ icons.
  • Serve dist/ locally with npm run preview and use Chrome DevTools → Application → Manifest (should show icons, installability ✓) and Service Workers (registered).
  • Install to desktop / "Add to Home Screen" on mobile.
  • Confirm the app still works fully offline after first load (airplane mode). Since there are no network calls, the only risk is the SW failing to precache — Workbox handles this.

Phase 2 — Firebase Hosting setup

Goal: serve dist/ over HTTPS on a *.web.app domain (or custom domain).

2.1 Install the CLI

npm install -D firebase-tools

2.2 Init

npx firebase init hosting

Answer:

  • Public directory: dist
  • Single-page app (rewrite all urls to /index.html): Yes — though the app has no client-side routing today, this protects future routes and is harmless.
  • Set up automatic builds with GitHub: No. (You're not using GitHub.)
  • File dist/index.html already exists — overwrite? No. This is critical — saying yes would clobber the Vite-built index.

This creates:

  • firebase.json
  • .firebaserc

2.3 firebase.json (target shape)

{
  "hosting": {
    "public": "dist",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [{ "source": "**", "destination": "/index.html" }],
    "headers": [
      {
        "source": "**/*.@(js|css|svg|png|ico|woff2|webmanifest)",
        "headers": [
          { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
        ]
      },
      {
        "source": "/sw.js",
        "headers": [
          { "key": "Cache-Control", "value": "no-cache" }
        ]
      },
      {
        "source": "/index.html",
        "headers": [
          { "key": "Cache-Control", "value": "no-cache" }
        ]
      }
    ]
  }
}

Why the headers: Vite hashes asset filenames, so JS/CSS/icons are content-addressed → cache them immortal. index.html and sw.js must stay fresh so updates propagate — no-cache revalidates them every time. (Note: with vite-plugin-pwa's autoUpdate, the SW self-updates; the no-cache on sw.js is belt-and-braces.)

2.4 First deploy

npm run build
npx firebase deploy --only hosting

Output: https://<project>.web.app. The mic will now work because the origin is HTTPS.

2.5 .gitignore

Add .firebase/ and firebase-debug.log (already in a sane gitignore, but confirm).


Phase 3 — (optional) custom domain

In Firebase console → Hosting → Add custom domain. Add the DNS TXT/CNAME records it gives you. Lets Encrypt cert is auto-provisioned. No app change needed.



Phase 4 — Web optimization checklist

Things that matter specifically for shipping this app on the web. Most are cheap; do them before the first public deploy.

4.1 Drop the dead dependency

audiomotion-analyzer is listed in package.json but imported nowhere in src/. Vite tree-shakes unused ESM, so it likely doesn't bloat the production bundle — but it still inflates npm install and is misleading. Either:

  • Remove it from dependencies (one-line npm uninstall audiomotion-analyzer), or
  • If something depends on it being present (it doesn't — grep confirms zero imports), wire it up.

Recommend: remove.

4.2 Audit bundle size

npm run build
npx vite-bundle-visualizer   # one-off, no config needed

Expected heavy bits: pitchy (small, pure JS) and React 19. electron / electron-builder are devDeps and excluded from the web build. If the visualizer shows anything surprising (e.g. a transitive dep pulling in moment/lodash), trim it.

4.3 Code-split the heavy, rarely-used views

DebugView, DrumView, and Tuner are collapsible panels most users never open. They import theory helpers and own audio contexts. Lazy-load them so the initial bundle stays lean:

const Tuner = lazy(() => import('./components/Tuner'))
const DebugView = lazy(() => import('./components/DebugView'))
const DrumView = lazy(() => import('./components/DrumView'))

Wrap in <Suspense fallback={null}>. These are already behind toggle buttons, so there's no UX cost. This is the single biggest web win — the core pitch/chord flow loads first.

4.4 Audio permissions & UX on the web

  • HTTPS — covered by Firebase. getUserMedia is blocked on plain HTTP (except localhost).
  • User gesture — the existing "Start Listening" button already provides the gesture the browser requires to call getUserMedia. Don't change this; never auto-start listening on load.
  • Mobile Safari quirks — iOS Safari:
    • Requires audio: { echoCancellation: false } style may differ; test the existing AudioCapture constraints on an iPhone.
    • AudioContext must be resumed after a gesture — already handled since capture starts on click.
    • sharedArrayBuffer is not needed by this app (no worklets), so no COOP/COEP headers required. Good — don't add them; they'd complicate the Firebase static host.
  • Permissions API — optionally surface "microphone blocked" before the user clicks, using navigator.permissions.query({ name: 'microphone' }). The existing micError state already covers the denied case; this is a polish step, not required.

4.5 Mobile layout

The app uses Tailwind with responsive lg: breakpoints already (e.g. the progressions sidebar is hidden lg:block). For a real mobile deploy:

  • Test the fretboard / piano SVGs at phone widths — they're SVG so they scale, but text labels may crowd.
  • The controls bar wraps with flex-wrap — good. Verify the key-lock select dropdowns are tappable.
  • Consider a display: standalone install prompt; the manifest already sets standalone.
  • This is a "play and look at the screen" app — portrait guitar fretboard orientation may want orientation: 'portrait' in the manifest instead of 'any'. Decide based on the primary instrument view.

4.6 Performance for the audio hot loop

Already mostly tuned (the dual-analyser design is deliberate). Web-specific notes:

  • requestAnimationFrame driving the analyser is fine on web — no change.
  • Float32Array chroma math is cheap. No change needed.
  • Don't add a Workbox runtime cache for audio — there's no audio to cache; everything is live.
  • If you ever add an AudioWorklet, its file must be served with the correct MIME and same-origin — Workbox precaches it as part of globPatterns only if it's emitted to dist/. Not needed today.

4.7 SEO / social (minimal — this is an app, not content)

  • <title> and meta description (added in 1.4) are enough for the web.app URL to have a sane share card.
  • Add og:title / og:description / og:image to index.html if you want a nice link preview (point og:image to /icon-512.png). Optional.
  • No sitemap needed for a single-page offline app.

4.8 Keep Electron working

The desktop build must not regress:

  • electron-builder config in package.json is untouched by any of the above.
  • The CSP change in index.html affects both targets — verify the desktop window still loads (it serves the built dist/index.html, so the new production CSP applies there too; that's fine and arguably better).
  • vite-plugin-pwa is a dev dependency; it only runs at build time and doesn't touch the renderer runtime in a way that breaks Electron.
  • Run npm run electron:dev after Phase 1 to confirm the Electron path still works.

What stays Electron-only

Nothing in this plan removes or forks code. The Electron and web builds share the exact same dist/ output. The only per-target differences are:

  • Electron adds the electron/main.cjs shell and electron-builder packaging.
  • Web adds the PWA manifest + SW (ignored by Electron) and is served over HTTPS.

If a future feature needs a native API (filesystem dialogs, auto-launch, tray), it would go through Electron's preload/IPC — which currently exposes nothing. At that point you'd add an isElectron guard and a preload IPC bridge. Not needed for any current feature.


Suggested order of operations

  1. Phase 1.2 — create public/, drop in icons (Phase 1.3).
  2. Phase 1.1 + 1.4 — add vite-plugin-pwa, update index.html, relax CSP.
  3. Verify with npm run preview + DevTools (Phase 1.5).
  4. Phase 2firebase init hosting, hand-tune firebase.json, first deploy.
  5. Phase 4.1 — remove dead audiomotion-analyzer dep.
  6. Phase 4.3 — lazy-load the three collapsible views.
  7. Polish: mobile pass (4.5), bundle audit (4.2), optional OG tags (4.7).

Each step is independently revertible. Stop after step 4 and you have a working, installable, offline PWA on the web; the rest is optimization.