Skip to content

Building PWAs with Faststrap

Faststrap provides a production-safe PWA baseline through add_pwa():

  • Manifest generation (/manifest.json)
  • Service worker route (/sw.js)
  • Automatic service worker registration
  • Offline fallback route (/offline)
  • Runtime caching for better resilience

Accessibility note: - Faststrap keeps mobile zoom enabled by default (width=device-width, initial-scale=1).

Quick Start

from fasthtml.common import FastHTML
from faststrap import add_bootstrap, add_pwa

app = FastHTML()
add_bootstrap(app)

add_pwa(
    app,
    name="My App",
    short_name="MyApp",
    theme_color="#0d6efd",
    icon_path="/assets/icon.png",
)

Icon note: - Use a square PNG icon for icon_path when you want reliable install behavior across iOS and Chromium browsers. - PwaMeta() and add_pwa() share the same default icon path, /assets/icon.png, so direct meta usage and manifest generation stay aligned.

What add_pwa() Configures

  1. Injects required PWA/mobile meta tags.
  2. Serves a generated manifest.json.
  3. Serves a robust service worker script at /sw.js.
  4. Injects service worker registration code into app headers.
  5. Serves /offline page (enabled by default).
  6. Respects scope for scoped deployments (for example /myapp/sw.js).

Default Offline/Caching Strategy

The built-in service worker uses:

  • Install: tolerant pre-cache (Promise.allSettled) so one failed URL does not break install.
  • Navigation requests: network-first with cache fallback, then /offline.
  • Static assets (css, js, images, fonts): stale-while-revalidate.
  • Other GET requests: network-first with runtime cache write-through.

This is a practical baseline for production apps that need reliable offline fallback behavior.

Advanced Configuration

add_pwa() now supports cache controls:

add_pwa(
    app,
    cache_name="myapp-cache",
    cache_version="v2026-02-23",
    pre_cache_urls=(
        "/health",
        "/assets/logo.png",
    ),
)
  • cache_name: prefix for cache storage
  • cache_version: version suffix for cache invalidation on deploy
  • pre_cache_urls: additional URLs to pre-cache

Faststrap still pre-caches its core defaults and /offline.

Background Sync Foundation (Opt-in)

Faststrap includes a lightweight background sync scaffold that you can enable without committing to a queue implementation yet:

add_pwa(
    app,
    enable_background_sync=True,
    background_sync_tag="faststrap-background-sync",
)
  • enable_background_sync: enables service worker sync event hooks
  • background_sync_tag: tag used when registering sync tasks

This is intentionally a foundation layer for v0.6.x work; request queue persistence/replay is still application-defined.

Route-Aware Cache Policies (Opt-in)

You can define route prefix policies in the generated service worker:

add_pwa(
    app,
    route_cache_policies={
        "/api/public/": "stale-while-revalidate",
        "/assets/": "cache-first",
    },
)

Supported strategies:

  • network-first
  • stale-while-revalidate
  • cache-first

Push Foundation (Opt-in)

Enable push event scaffolding in the generated service worker:

add_pwa(
    app,
    enable_push=True,
    default_push_title="My App Notification",
)

This adds push and notificationclick handlers with safe defaults.

When to Use a Custom Service Worker

Use a custom sw.js when you need:

  • fine-grained API caching rules
  • background sync
  • push notifications
  • per-route cache policies

In that case:

  1. pass service_worker=False to add_pwa()
  2. mount your own /sw.js route

Mobile Components

Faststrap also includes mobile-oriented UI components:

  • BottomNav, BottomNavItem
  • Sheet
  • InstallPrompt

API Reference

faststrap.pwa.core.PwaMeta(name=None, short_name=None, theme_color='#ffffff', background_color='#ffffff', description=None, icon_path=_DEFAULT_ICON_PATH, icon_192=None, icon_512=None, manifest_path='/manifest.json')

Generate PWA meta tags and link elements.

These tags are essential for: - Installing the app on mobile home screens - Setting the theme color of the browser bar - Defining icons for different platforms (iOS/Android)

Parameters:

Name Type Description Default
name str | None

Full name of the application

None
short_name str | None

Short name for home screen (12 chars max recommended)

None
theme_color str

Color of the browser toolbar

'#ffffff'
background_color str

Background color for splash screen

'#ffffff'
description str | None

Description of the app

None
icon_path str

Path to the main icon (should be square, ideally 512x512)

_DEFAULT_ICON_PATH
icon_192 str | None

Optional dedicated 192x192 icon path

None
icon_512 str | None

Optional dedicated 512x512 icon path

None
manifest_path str

URL path to the manifest file

'/manifest.json'

Returns:

Type Description
tuple[Any, ...]

Tuple of FastHTML Meta and Link elements

Source code in src/faststrap/pwa/core.py
def PwaMeta(
    name: str | None = None,
    short_name: str | None = None,
    theme_color: str = "#ffffff",
    background_color: str = "#ffffff",
    description: str | None = None,
    icon_path: str = _DEFAULT_ICON_PATH,
    icon_192: str | None = None,
    icon_512: str | None = None,
    manifest_path: str = "/manifest.json",
) -> tuple[Any, ...]:
    """
    Generate PWA meta tags and link elements.

    These tags are essential for:
    - Installing the app on mobile home screens
    - Setting the theme color of the browser bar
    - Defining icons for different platforms (iOS/Android)

    Args:
        name: Full name of the application
        short_name: Short name for home screen (12 chars max recommended)
        theme_color: Color of the browser toolbar
        background_color: Background color for splash screen
        description: Description of the app
        icon_path: Path to the main icon (should be square, ideally 512x512)
        icon_192: Optional dedicated 192x192 icon path
        icon_512: Optional dedicated 512x512 icon path
        manifest_path: URL path to the manifest file

    Returns:
        Tuple of FastHTML Meta and Link elements
    """
    primary_icon = icon_192 or icon_path
    tile_icon = icon_512 or icon_path
    tags = [
        # Basic Mobile Meta
        Meta(
            name="viewport",
            content="width=device-width, initial-scale=1",
        ),
        Meta(name="theme-color", content=theme_color),
        Meta(name="mobile-web-app-capable", content="yes"),
        # iOS Specific
        Meta(name="apple-mobile-web-app-capable", content="yes"),
        Meta(name="apple-mobile-web-app-status-bar-style", content="black-translucent"),
        Meta(name="apple-mobile-web-app-title", content=short_name or name or "App"),
        Link(rel="apple-touch-icon", href=primary_icon),
        # Windows
        Meta(name="msapplication-TileColor", content=theme_color),
        Meta(name="msapplication-TileImage", content=tile_icon),
        # Manifest
        Link(rel="manifest", href=manifest_path),
    ]

    if description:
        tags.append(Meta(name="description", content=description))

    return tuple(tags)

faststrap.pwa.core.add_pwa(app, name='Faststrap App', short_name='Faststrap', description='A Progressive Web App built with Faststrap', theme_color='#ffffff', background_color='#ffffff', icon_path=_DEFAULT_ICON_PATH, icon_192=None, icon_512=None, display='standalone', start_url='/', scope='/', service_worker=True, offline_page=True, cache_name='faststrap-app', cache_version='v1', pre_cache_urls=None, use_cdn=None, enable_background_sync=False, background_sync_tag='faststrap-background-sync', route_cache_policies=None, enable_push=False, default_push_title='Faststrap Notification')

Enable PWA capabilities for the FastHTML app.

This helper: 1. Injects PWA meta tags into app headers 2. Serves a generated manifest.json 3. Serves a standard sw.js Service Worker (if enabled) 4. serves an /offline route (if enabled)

Parameters:

Name Type Description Default
app Any

FastHTML application instance

required
name str

App name

'Faststrap App'
short_name str

App short name

'Faststrap'
description str

App description

'A Progressive Web App built with Faststrap'
theme_color str

Theme color

'#ffffff'
background_color str

Splash screen background color

'#ffffff'
icon_path str

Path to icon file

_DEFAULT_ICON_PATH
icon_192 str | None

Optional dedicated 192x192 icon path for the manifest and touch icon

None
icon_512 str | None

Optional dedicated 512x512 icon path for the manifest/tile icon

None
display str

Display mode (standalone, fullscreen, minimal-ui, browser)

'standalone'
start_url str

URL to open on launch

'/'
scope str

Scope of the PWA

'/'
service_worker bool

Enable automatic Service Worker

True
offline_page bool

Enable automatic /offline route

True
cache_name str

Service worker cache name prefix

'faststrap-app'
cache_version str

Cache version suffix used for cache invalidation

'v1'
pre_cache_urls Sequence[str] | None

Optional extra URLs to precache (in addition to defaults)

None
use_cdn bool | None

Force CDN-aware precache defaults; when omitted this is auto-detected

None
enable_background_sync bool

Enable Background Sync foundation hooks

False
background_sync_tag str

Tag used for Background Sync registrations

'faststrap-background-sync'
route_cache_policies dict[str, str] | None

Optional route prefix -> strategy mapping values: "network-first", "stale-while-revalidate", "cache-first"

None
enable_push bool

Enable push notification service worker handlers

False
default_push_title str

Fallback push notification title

'Faststrap Notification'
Source code in src/faststrap/pwa/core.py
def add_pwa(
    app: Any,
    name: str = "Faststrap App",
    short_name: str = "Faststrap",
    description: str = "A Progressive Web App built with Faststrap",
    theme_color: str = "#ffffff",
    background_color: str = "#ffffff",
    icon_path: str = _DEFAULT_ICON_PATH,
    icon_192: str | None = None,
    icon_512: str | None = None,
    display: str = "standalone",
    start_url: str = "/",
    scope: str = "/",
    service_worker: bool = True,
    offline_page: bool = True,
    cache_name: str = "faststrap-app",
    cache_version: str = "v1",
    pre_cache_urls: Sequence[str] | None = None,
    use_cdn: bool | None = None,
    enable_background_sync: bool = False,
    background_sync_tag: str = "faststrap-background-sync",
    route_cache_policies: dict[str, str] | None = None,
    enable_push: bool = False,
    default_push_title: str = "Faststrap Notification",
) -> None:
    """
    Enable PWA capabilities for the FastHTML app.

    This helper:
    1. Injects PWA meta tags into app headers
    2. Serves a generated `manifest.json`
    3. Serves a standard `sw.js` Service Worker (if enabled)
    4. serves an `/offline` route (if enabled)

    Args:
        app: FastHTML application instance
        name: App name
        short_name: App short name
        description: App description
        theme_color: Theme color
        background_color: Splash screen background color
        icon_path: Path to icon file
        icon_192: Optional dedicated 192x192 icon path for the manifest and touch icon
        icon_512: Optional dedicated 512x512 icon path for the manifest/tile icon
        display: Display mode (standalone, fullscreen, minimal-ui, browser)
        start_url: URL to open on launch
        scope: Scope of the PWA
        service_worker: Enable automatic Service Worker
        offline_page: Enable automatic /offline route
        cache_name: Service worker cache name prefix
        cache_version: Cache version suffix used for cache invalidation
        pre_cache_urls: Optional extra URLs to precache (in addition to defaults)
        use_cdn: Force CDN-aware precache defaults; when omitted this is auto-detected
        enable_background_sync: Enable Background Sync foundation hooks
        background_sync_tag: Tag used for Background Sync registrations
        route_cache_policies: Optional route prefix -> strategy mapping
                              values: "network-first", "stale-while-revalidate", "cache-first"
        enable_push: Enable push notification service worker handlers
        default_push_title: Fallback push notification title
    """

    normalized_scope = _normalize_scope(scope)
    manifest_path = _join_scope_path(normalized_scope, "/manifest.json")
    sw_path = _join_scope_path(normalized_scope, "/sw.js")
    offline_path = _join_scope_path(normalized_scope, "/offline")

    # 1. Inject Headers
    pwa_headers = PwaMeta(
        name=name,
        short_name=short_name,
        theme_color=theme_color,
        background_color=background_color,
        description=description,
        icon_path=icon_path,
        icon_192=icon_192,
        icon_512=icon_512,
        manifest_path=manifest_path,
    )

    # Append to existing headers (similar logic to add_bootstrap)
    current_hdrs = list(getattr(app, "hdrs", []))
    app.hdrs = current_hdrs + list(pwa_headers)

    # 2. Serve Manifest
    icon_192_src = icon_192 or icon_path
    icon_512_src = icon_512 or icon_path
    manifest_data = {
        "name": name,
        "short_name": short_name,
        "description": description,
        "theme_color": theme_color,
        "background_color": background_color,
        "display": display,
        "start_url": start_url,
        "scope": scope,
        "icons": [
            {
                "src": icon_192_src,
                "sizes": "192x192",
                "type": "image/png",
            },
            {
                "src": icon_512_src,
                "sizes": "512x512",
                "type": "image/png",
            },
        ],
    }

    @app.get(manifest_path)
    def manifest() -> Any:
        return JSONResponse(manifest_data)

    # 3. Serve Service Worker
    if service_worker:
        # Build robust service worker script with safe defaults and optional extension points.
        resolved_use_cdn = _app_uses_cdn_assets(app) if use_cdn is None else use_cdn
        deduped_precache = list(
            dict.fromkeys(
                [
                    *_default_precache_urls(use_cdn=resolved_use_cdn),
                    *(pre_cache_urls or []),
                    offline_path,
                ]
            )
        )
        sw_script = _render_sw_script(
            cache_name=cache_name,
            cache_version=cache_version,
            pre_cache_urls=deduped_precache,
            offline_fallback_path=offline_path,
            enable_background_sync=enable_background_sync,
            background_sync_tag=background_sync_tag,
            route_cache_policies=route_cache_policies,
            enable_push=enable_push,
            default_push_title=default_push_title,
        )

        @app.get(sw_path)
        def sw() -> Any:
            return Response(sw_script, media_type="application/javascript")

        # Register the SW in the app (inject script)
        reg_script = Script(
            _build_sw_register_script(
                sw_path=sw_path,
                scope=normalized_scope,
                enable_background_sync=enable_background_sync,
                background_sync_tag=background_sync_tag,
            )
        )
        app.hdrs = list(app.hdrs) + [reg_script]

    # 4. Serve Offline Page
    if offline_page:

        @app.get(offline_path)
        def offline() -> Any:
            return (
                Title("Offline - " + name),
                EmptyState(
                    title="No Internet Connection",
                    description="You are currently offline. Please check your connection and try again.",
                    icon="wifi-slash",
                    action_text="Retry",
                    action_href=start_url,  # Try going home
                    cls="min-vh-100 d-flex align-items-center justify-content-center",
                ),
            )