PWA Introduction
Purpose β This document describes the Progressive Web App (PWA) capabilities of Mixtape Society's public sharing feature.
It explains how the service worker enables offline playback, how audio caching works with range requests, the installation flow, and how the manifest scopes the PWA to public routes only.
All statements below are verified against the current source files (service-worker.js, static/js/pwa/pwa-manager.js, and manifest.json).
π High-Level OverviewΒΆ
| Responsibility | Implementation |
|---|---|
Register service worker scoped to /play/ |
pwa-manager.js β registerServiceWorker() |
| Cache static assets (CSS, JS, images) | Service worker β STATIC_ASSETS array |
| Cache audio files for offline playback | Service worker β handleAudioRequest() |
| Handle HTTP range requests (seeking) | Service worker β bypass cache for 206 responses |
| Cache mixtape pages and metadata | Service worker β handleMixtapePage() |
| Manage cache storage and limits | pwa-manager.js β getCacheSize(), clearCache() |
| Handle app installation prompts | pwa-manager.js β setupInstallPrompt() |
| Detect online/offline status | pwa-manager.js β setupNetworkDetection() |
Scope RestrictionΒΆ
The PWA functionality is intentionally scoped to /play/ routes only. Authenticated routes (/mixtapes, /editor, etc.) always require live database access and are never cached.
πΊοΈ PWA ArchitectureΒΆ
flowchart TD
subgraph Browser
UI["Web UI<br/>/play/share/slug"]
SW["Service Worker<br/>scope: /play/"]
Cache["Cache Storage"]
end
subgraph Server
Flask["Flask App"]
Audio["Audio Files"]
Meta["Mixtape JSON"]
end
UI -->|Request| SW
SW -->|Cache Hit| Cache
SW -->|Cache Miss| Flask
Flask -->|Stream| Audio
Flask -->|Metadata| Meta
SW -->|Store| Cache
style Browser stroke:#4CAF50,stroke-width:2px
style Server stroke:#2196F3,stroke-width:2px
π¦ Core ComponentsΒΆ
1. Service Worker (service-worker.js)ΒΆ
Located at app root, handles all offline caching logic.
Cache Strategy by Resource TypeΒΆ
| Resource Type | Strategy | Cache Name | Why |
|---|---|---|---|
| Static assets (CSS/JS) | Cache-first | mixtape-static-v1.0.0 |
Instant page loads |
| Audio files | Cache-first | mixtape-audio-v1.0.0 |
Offline playback |
| Cover images | Cache-first | mixtape-images-v1.0.0 |
Fast thumbnails |
| Mixtape pages | Network-first | mixtape-metadata-v1.0.0 |
Fresh content when online |
| CDN resources | Cache-first | mixtape-static-v1.0.0 |
Bootstrap, Vibrant.js, etc. |
Range Request HandlingΒΆ
- Full file requests (no
Rangeheader) β Cached as 200 OK - Partial requests (
Rangeheader present) β Bypass cache, serve 206 from server - Seeking always requires network (Cache API limitation)
2. PWA Manager (pwa-manager.js)ΒΆ
Client-side orchestration of PWA features.
Key Responsibilities:
class PWAManager {
registerServiceWorker() // Register SW with /play/ scope
setupInstallPrompt() // Capture beforeinstallprompt
setupNetworkDetection() // Online/offline indicators
downloadMixtapeForOffline() // Batch download tracks
showCacheManagement() // Storage UI modal
clearCache(type) // Delete cached resources
}
3. Manifest (Dynamic per Mixtape)ΒΆ
Each mixtape generates its own PWA manifest dynamically at /play/share/{slug}/manifest.json.
Why Dynamic Manifests?
- β Each mixtape can be installed as its own PWA
- β PWA icon shows the mixtape's cover art
- β PWA name is the mixtape's title
- β Opening the PWA goes directly to that specific mixtape
- β No 404 errors when launching from home screen
Example Dynamic Manifest:
{
"name": "Summer Vibes 2024",
"short_name": "Summer Vibe",
"description": "A mixtape with 12 tracks",
"start_url": "/play/share/summer-vibes-2024",
"scope": "/play/",
"display": "standalone",
"background_color": "#198754",
"theme_color": "#198754",
"icons": [
{
"src": "/play/covers/summer-cover.jpg",
"sizes": "512x512",
"type": "image/jpeg",
"purpose": "any maskable"
}
]
}
Backend Implementation:
# play.py - Dynamic manifest endpoint
@play.route("/share/<slug>/manifest.json")
def mixtape_manifest(slug: str) -> Response:
"""Generate dynamic PWA manifest for specific mixtape"""
mixtape = mixtape_manager.get(slug)
if not mixtape:
abort(404)
# Use mixtape cover as PWA icon
icon_url = f"/play/covers/{mixtape['cover'].split('/')[-1]}" \
if mixtape.get('cover') \
else "/static/icons/icon-512.png"
manifest = {
"name": mixtape.get('title', 'Mixtape'),
"short_name": mixtape.get('title', 'Mixtape')[:12],
"description": f"A mixtape with {len(mixtape.get('tracks', []))} tracks",
"start_url": f"/play/share/{slug}",
"scope": "/play/",
"display": "standalone",
"background_color": "#198754",
"theme_color": "#198754",
"icons": [{
"src": icon_url,
"sizes": "512x512",
"type": "image/jpeg" if mixtape.get('cover') else "image/png",
"purpose": "any maskable"
}]
}
return Response(json.dumps(manifest, indent=2),
mimetype='application/manifest+json',
headers={'Cache-Control': 'public, max-age=3600'})
Template Integration:
<!-- play_mixtape.html - Dynamic manifest per mixtape -->
<link rel="manifest" href="/play/share/{{ mixtape.slug }}/manifest.json">
<meta name="apple-mobile-web-app-title" content="{{ mixtape.title }}">
Scope Restriction: All mixtapes share the /play/ scope, which means:
- β One service worker handles all mixtapes efficiently
- β Public mixtape receivers get offline capabilities
- β Authenticated creator tools always hit the database
- β No stale data in admin interfaces
- β Each mixtape still gets its own home screen icon and name
π Request Flow (Offline-Capable)ΒΆ
sequenceDiagram
participant Browser
participant ServiceWorker
participant Cache
participant Server
Browser->>ServiceWorker: GET /play/share/summer-vibes
ServiceWorker->>Cache: Check cache
alt Cache hit
Cache-->>ServiceWorker: Cached HTML
ServiceWorker-->>Browser: Instant response β‘
ServiceWorker->>Server: Update in background
else Cache miss
ServiceWorker->>Server: Fetch from network
Server-->>ServiceWorker: Fresh HTML
ServiceWorker->>Cache: Store for offline
ServiceWorker-->>Browser: Response
end
Note over Browser,Server: Audio Request (seeking)
Browser->>ServiceWorker: GET /play/audio?quality=medium<br/>Range: bytes=1000000-2000000
ServiceWorker->>Server: Bypass cache (Range request)
Server-->>ServiceWorker: 206 Partial Content
ServiceWorker-->>Browser: Stream audio
π΅ Audio Caching with Range RequestsΒΆ
The ChallengeΒΆ
HTTP range requests (used for audio seeking) return 206 Partial Content responses, which the Cache API cannot store. Only full 200 OK responses can be cached.
The SolutionΒΆ
- Detect range requests:
const isRangeRequest = request.headers.has('Range');
if (isRangeRequest) {
return fetch(request); // Bypass cache
}
- Cache only full files:
- Background caching requests full files:
User Experience ImpactΒΆ
| Scenario | Behavior |
|---|---|
| Playing uncached track | Streams from server, caches full file |
| Playing cached track | Instant playback from cache β‘ |
| Seeking in uncached track | Fetches partial range from server |
| Seeking in cached track | Fetches partial range from server* |
| Download for offline | Downloads complete files (200 OK) |
*Why seek needs network: Cache API limitation - cannot serve partial responses from cached full files.
π₯ Download for OfflineΒΆ
User Flow:
- User opens shared mixtape:
/play/share/summer-vibes - Clicks "Download for Offline" button
- PWA Manager downloads all tracks in parallel (3 at a time)
- Progress indicator shows completion
- All tracks now available offline
Implementation:
async downloadMixtapeForOffline() {
const tracks = window.__mixtapeData.tracks;
const quality = localStorage.getItem('audioQuality') || 'medium';
// Download in batches of 3
for (let i = 0; i < tracks.length; i += 3) {
const batch = tracks.slice(i, i + 3);
await Promise.allSettled(
batch.map(track => this.cacheAudioFile(track.path, quality))
);
}
}
async cacheAudioFile(path, quality) {
// Send message to service worker
return new Promise((resolve, reject) => {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event) => {
event.data.success ? resolve() : reject();
};
this.swRegistration.active.postMessage(
{ action: 'CACHE_AUDIO', data: { url: path, quality } },
[messageChannel.port2]
);
});
}
π§ Backend IntegrationΒΆ
Flask RoutesΒΆ
# In app.py - serve PWA files
@app.route('/service-worker.js')
def service_worker():
"""Serve service worker with proper headers"""
response = send_from_directory('.', 'service-worker.js',
mimetype='application/javascript')
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Service-Worker-Allowed'] = '/play/' # Scope restriction
return response
@app.route('/manifest.json')
def manifest():
"""Serve PWA manifest"""
return send_from_directory('.', 'manifest.json',
mimetype='application/manifest+json')
Template IntegrationΒΆ
PWA features are loaded only in play_mixtape.html, not in base.html:
<!-- play_mixtape.html only -->
{% block extra_meta %}
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#198754">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
{% endblock %}
{% block extra_js %}
<script type="module" src="{{ url_for('static', filename='js/pwa/pwa-manager.js') }}"></script>
{% endblock %}
This ensures PWA only activates for public mixtape pages, not authenticated routes.
π Cache ManagementΒΆ
Storage LimitsΒΆ
// Check storage quota
const estimate = await navigator.storage.estimate();
console.log(`Using ${estimate.usage} of ${estimate.quota} bytes`);
// Typical: ~60% of free disk space on Chrome
Cache ClearingΒΆ
User InterfaceΒΆ
- Storage management modal shows cache breakdown
- Clear buttons for specific cache types
- Automatic cleanup when quota reached
ProgrammaticΒΆ
// Clear audio cache only
await pwaManager.clearCache('audio');
// Clear everything
await pwaManager.clearCache('all');
π Update HandlingΒΆ
Service Worker UpdatesΒΆ
Automatic DetectionΒΆ
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New version available!
showUpdateNotification();
}
});
});
User PromptΒΆ
<div class="update-notification">
Update available
<button onclick="updateServiceWorker()">Update Now</button>
</div>
Mixtape Content UpdatesΒΆ
When a mixtape changes (tracks added/removed/reordered), the service worker uses network-first strategy for mixtape pages:
- Fetch from server (get latest version)
- If successful, update cache
- If offline, serve cached version
- Show "viewing cached version" indicator
π± Installation Flow (Per-Mixtape PWAs)ΒΆ
Each shared mixtape can be installed as its own standalone app with a unique icon and name.
Desktop (Chrome/Edge)ΒΆ
- User visits
/play/share/summer-vibes-2024 - Browser shows install icon in address bar
- Click β "Install Summer Vibes 2024" (uses mixtape title)
- App opens in standalone window
- Icon on taskbar shows mixtape cover art
Mobile (Android)ΒΆ
- User visits shared mixtape link
- Chrome shows "Add Summer Vibes 2024 to Home Screen" banner
- Or tap menu β "Install App"
- Icon appears on home screen with mixtape cover as icon
- Launches directly to that mixtape (no navigation needed)
- Can install multiple mixtapes - each gets its own icon
Mobile (iOS)ΒΆ
- User visits shared mixtape link
- Tap Share button
- Select "Add to Home Screen"
- Edit name if desired (defaults to mixtape title)
- Icon appears on home screen
- Opens directly to the mixtape
Installation BenefitsΒΆ
Before (Static Manifest):
- β 404 errors when launching from home screen
- β Generic "Mixtape Society" icon
- β Opens to main page, user has to navigate
- β Confusing if multiple mixtapes installed
After (Dynamic Manifests):
- β Direct launch to specific mixtape
- β Unique cover art as icon
- β Clear mixtape title as app name
- β Can install many mixtapes without confusion
- β Perfect for gift mixtapes - truly personal
Multiple InstallationsΒΆ
Users can install as many mixtapes as they want:
Home Screen:
ββββββββββββββ¬βββββββββββββ¬βββββββββββββ
β Summer β Road Trip β Chill β
β Vibes 2024 β Mix β Sundays β
β [cover 1] β [cover 2] β [cover 3] β
ββββββββββββββ΄βββββββββββββ΄βββββββββββββ
Each opens directly to its own mixtape - no confusion!
π§ͺ Testing & DebuggingΒΆ
Chrome DevToolsΒΆ
Application TabΒΆ
- Manifest: Verify icons and config
- Service Workers: Check registration status
- Cache Storage: Inspect cached resources
- Storage: Check quota usage
Offline TestingΒΆ
- Open DevTools β Application β Service Workers
- Check "Offline" box
- Reload page
- Should still load from cache β
Console CommandsΒΆ
// Check SW status
navigator.serviceWorker.getRegistration().then(reg => {
console.log('Scope:', reg.scope);
console.log('Active:', reg.active?.state);
});
// Check cache contents
caches.keys().then(keys => {
console.log('Caches:', keys);
keys.forEach(key => {
caches.open(key).then(cache => {
cache.keys().then(reqs => {
console.log(`${key}:`, reqs.length, 'items');
});
});
});
});
// Check storage
navigator.storage.estimate().then(est => {
const used = (est.usage / 1024 / 1024).toFixed(2);
const total = (est.quota / 1024 / 1024).toFixed(2);
console.log(`Storage: ${used} MB / ${total} MB`);
});
β οΈ Limitations & Known IssuesΒΆ
What Works OfflineΒΆ
β View mixtape page and metadata β See cover art and track list β Play downloaded tracks β Read liner notes
What Requires NetworkΒΆ
β Seeking in tracks (range requests)
β Downloading new tracks
β Checking for mixtape updates
β Authenticated routes (/mixtapes, /editor)
Browser SupportΒΆ
| Browser | Service Workers | Install | Offline |
|---|---|---|---|
| Chrome 90+ | β | β | β |
| Edge 90+ | β | β | β |
| Firefox 90+ | β | β οΈ Limited | β |
| Safari 14+ | β | β οΈ Manual | β |
π Performance MetricsΒΆ
Cache Hit RatesΒΆ
- Static assets: ~95% (after first visit)
- Cover images: ~90%
- Audio files: ~60% (depends on downloads)
- Mixtape pages: ~80%
Load TimesΒΆ
- First visit: ~2-3s (network dependent)
- Return visit (cached): ~100-300ms β‘
- Offline visit: ~100ms β‘
π Security ConsiderationsΒΆ
HTTPS RequirementΒΆ
Service workers require HTTPS in production (localhost exempted for development).
Scope IsolationΒΆ
The /play/ scope ensures authenticated routes cannot be hijacked or cached inappropriately.
Content SecurityΒΆ
- No user-generated code in service worker
- Cache keys validated before storage
- CORS headers properly configured
π Related DocumentationΒΆ
- Audio Streaming - How audio streaming works
- Mixtape Manager - Mixtape data persistence
- Configuration - Environment setup
