djust docs
Browse documentation
intermediate

Progressive Web App (PWA) Support

Add offline support, service workers, and installability with PWAMixin

On this page

djust provides built-in PWA support for offline-first applications with automatic synchronization, service worker generation, and offline-aware template directives.

What You Get

  • Service worker integration -- Automatic caching of HTML responses and static assets
  • Offline state management -- IndexedDB/LocalStorage abstraction via mixins
  • Optimistic UI updates -- Immediate feedback with sync when online
  • Offline directives -- dj-offline-hide, dj-offline-show, dj-offline-disable
  • Automatic manifest generation -- PWA manifest with customizable settings

Quick Start

1. Enable PWA in Your Templates

{% load djust_pwa %}
<!DOCTYPE html>
<html>
<head>
    {% djust_pwa_head name="My App" theme_color="#007bff" %}
</head>
<body>
    {% djust_offline_indicator offline_text="You're offline" %}
    {% djust_offline_styles %}

    <div dj-offline-hide>Only shown when online</div>
    <div dj-offline-show>Only shown when offline</div>
    <button dj-offline-disable dj-click="submit">Submit</button>
</body>
</html>

2. Use PWA Mixins in Your Views

from djust import LiveView
from djust.decorators import event_handler
from djust.pwa.mixins import PWAMixin, OfflineMixin

class MyView(OfflineMixin, LiveView):
    template_name = 'app.html'

    def mount(self, request, **kwargs):
        # No enable call: inheriting OfflineMixin is what enables offline mode.
        self.items = self.storage.get('items', [])

    @event_handler()
    def add_item(self, name: str = "", **kwargs):
        created = self.create_offline('Item', {'name': name})
        self.items.append(created)
        self.storage.set('items', self.items)
        self.sync_when_online()

3. Generate the Service Worker

python manage.py generate_sw

You can also serve the service worker from a view instead of a generated file:

# urls.py
from djust.pwa import service_worker_view

urlpatterns = [
    path("sw.js", service_worker_view),
    # ...
]

service_worker_view reads the DJUST_CONFIG['PWA_*'] keys described below.

PWA Mixins

PWAMixin

Base mixin for PWA functionality:

MethodDescription
get_pwa_config()PWA config dict injected into the template context
register_pwa_handlers()Register the install/update event handlers
handle_install_prompt()Called when the install prompt is shown
handle_app_update(version)Called when a new app version is available

There is no enable_offline() / disable_offline() / is_offline_enabled() — inheriting the mixin is what turns the behaviour on.

OfflineMixin

Enhanced offline state management. It does not subclass PWAMixin — combine them explicitly (class V(OfflineMixin, PWAMixin, LiveView)) if you want both.

MethodDescription
storageProperty — the OfflineStorage instance (.get(key, default) / .set(key, value))
sync_queueProperty — the pending SyncQueue
create_offline(model, data)Queue a create for sync; returns the optimistic record
update_offline(model, obj_id, data)Queue an update for sync
delete_offline(model, obj_id)Queue a delete for sync
get_cached_or_fetch(key, queryset)Serve from cache, else evaluate the queryset
sync_when_online()Drain the sync queue
get_offline_state()Current offline state dict
is_online()Always True server-side — see the note below
handle_connection_change(online)Connection-change hook (no built-in caller)

There is no save_offline_state() / load_offline_state() / handle_online(); use the storage property directly.

SyncMixin

Automatic background synchronization. It does not subclass OfflineMixin, and it depends on storage / sync_queue being supplied by one — list it after OfflineMixin (class V(SyncMixin, OfflineMixin, LiveView)), or the properties raise AttributeError.

MethodDescription
sync_queueProperty — the pending SyncQueue; enqueue via create_offline() / update_offline() / delete_offline()
sync_managerProperty — the SyncManager that runs the sync
sync_create_<Model>(data), sync_update_<Model>(obj_id, data), sync_delete_<Model>(obj_id)Your hooks, e.g. sync_create_Item; return truthy on success. Case-sensitive (built as f"sync_create_{action.model}")

Template Tags

{% djust_pwa_head %}

Complete PWA setup in one tag (manifest + service worker registration):

{% djust_pwa_head name="My App" theme_color="#007bff" %}

{% djust_pwa_manifest %}

Generate the PWA manifest link:

{% djust_pwa_manifest name="My App" short_name="App"
   theme_color="#007bff" background_color="#ffffff" display="standalone" %}

{% djust_sw_register %}

Register the service worker:

{% djust_sw_register sw_url="/sw.js" scope="/" %}

{% djust_offline_indicator %}

Visual offline status banner:

{% djust_offline_indicator offline_text="You're offline" show_when="offline" %}

show_when is "offline" (the default: shown only while offline), "online" (shown only while online) or "always". The client swaps the indicator's text between online_text and offline_text, and its class between online_class and offline_class (default djust-status-online / djust-status-offline), whenever the browser's network state changes. The server renders the state show_when implies, so the first paint is styled. The "offline" indicator hides itself with its own CSS until the page is offline; the "online" one relies on the dj-offline-hide rule from {% djust_pwa_head %} or {% djust_offline_styles %}. (Before 1.2.1 the "offline" indicator never appeared and the text and class never changed.)

Offline Directives

DirectiveBehavior
dj-offline-hideHide element when offline
dj-offline-showShow element only when offline
dj-offline-disableDisable form element when offline
dj-offline-queuedNot implemented — no client code reads this attribute; listed here only so it is not mistaken for a working directive

The client keeps djust-online or djust-offline on <body>, set at page load from navigator.onLine and updated on the browser's online / offline events; the directives are CSS rules on those classes, emitted by {% djust_pwa_head %} or {% djust_offline_styles %}, so include one of them. This is browser network state: a WebSocket reconnect does not count as offline. dj-offline-show elements stay hidden until the body is marked offline, including before the client has run, so they do not flash on load; a page that includes the directive CSS but never loads djust's client keeps them hidden, as it already did dj-offline-hide elements. (Before 1.2.1 nothing set the classes, so dj-offline-hide elements were always hidden, dj-offline-disable elements always disabled, and bare dj-offline-show elements always visible, online or not.)

<div dj-offline-hide>
    <button dj-click="save_to_server">Save</button>
</div>
<div dj-offline-show>
    <p>Changes will sync when you're back online.</p>
</div>

Service Worker Configuration

There are two settings surfaces, and which one applies depends on the consumer:

  • DJUST_CONFIG flat PWA_* keys (below) are read by PWAMixin.get_pwa_config(), PWAManifestGenerator / manifest_view, and ServiceWorkerGenerator / service_worker_view. The two views only take effect once you route them in urls.py.
  • Plain Django settings DJUST_PWA_* are what the template tags ({% djust_pwa_head %}, {% djust_pwa_manifest %}) and generate_sw read. They do not read DJUST_CONFIG.

There is no DJUST_PWA dict setting.

# settings.py
DJUST_CONFIG = {
    # Manifest
    "PWA_NAME": "My Application",
    "PWA_SHORT_NAME": "MyApp",
    "PWA_DESCRIPTION": "A djust-powered app",
    "PWA_THEME_COLOR": "#007bff",
    "PWA_BACKGROUND_COLOR": "#ffffff",
    "PWA_DISPLAY": "standalone",          # fullscreen | standalone | minimal-ui
    "PWA_ICONS": [
        {"src": "/static/icons/icon-192.png", "sizes": "192x192", "type": "image/png"},
    ],

    # Service worker
    "PWA_CACHE_NAME": "djust-v1",
    "PWA_CACHE_STRATEGY": "cache_first",  # or "network_first" / "stale_while_revalidate"
    "PWA_PRECACHE_URLS": ["/static/css/app.css", "/static/js/app.js"],
    "PWA_OFFLINE_PAGE": "/offline/",
    "PWA_CACHE_DURATION": 86400,
    "PWA_ENABLE_BACKGROUND_SYNC": False,
    "PWA_SYNC_ENDPOINT": "/djust/pwa/sync/",
}

Unknown PWA_CACHE_STRATEGY values silently fall back to cache_first.

The template tags take their values from their arguments ({% djust_pwa_head name="My App" theme_color="#007bff" %}) and fall back to these plain Django settings when an argument is not passed: DJUST_PWA_NAME, DJUST_PWA_SHORT_NAME, DJUST_PWA_DESCRIPTION, DJUST_PWA_THEME_COLOR, DJUST_PWA_BACKGROUND_COLOR.

Offline Storage

Structured offline data goes through a configurable backend, chosen once for the project:

DJUST_CONFIG = {
    "PWA_OFFLINE_STORAGE": "indexeddb",  # or "localstorage"
}

indexeddb is the default. What a view sets is not a backend selector:

class TodoView(OfflineMixin, LiveView):
    offline_storage = "todos"   # a NAMESPACE, not a backend

offline_storage is passed through as storage_name= — it names the key space this view persists under, so two views with different values keep separate data on the same backend. To change where data is stored, set PWA_OFFLINE_STORAGE; to change which slice of it a view owns, set offline_storage.

Example: Offline Todo App

from djust import LiveView
from djust.decorators import event_handler
from djust.pwa.mixins import OfflineMixin

class TodoView(OfflineMixin, LiveView):
    template_name = 'todos.html'

    def mount(self, request, **kwargs):
        self.todos = self.storage.get('todos', [])

    @event_handler()
    def add_todo(self, text: str = "", **kwargs):
        todo = self.create_offline('Todo', {'text': text, 'done': False})
        self.todos.append(todo)
        self.storage.set('todos', self.todos)
        self.sync_when_online()

    @event_handler()
    def toggle_todo(self, todo_id: str = "", **kwargs):
        for todo in self.todos:
            if todo['id'] == todo_id:
                todo['done'] = not todo['done']
                self.update_offline('Todo', todo_id, {'done': todo['done']})
        self.storage.set('todos', self.todos)
        self.sync_when_online()

Management Commands

# Basic generation
python manage.py generate_sw

# Custom output path
python manage.py generate_sw --output static/custom-sw.js

# Include static files in the cache
python manage.py generate_sw --cache-static

# Custom cache version (--sw-version: Django reserves --version)
python manage.py generate_sw --sw-version 2.1.0

Adding PWA to an Existing App

  1. Add {% load djust_pwa %} to your base template
  2. Include {% djust_pwa_head %} in your <head>
  3. Mix PWAMixin or OfflineMixin into your LiveViews
  4. Serve the service worker: run python manage.py generate_sw, or route djust.pwa.service_worker_view at /sw.js
  5. Deploy with HTTPS (required for service workers in production)

Browser Support

BrowserSupport
Chrome / EdgeFull
FirefoxFull
SafariPartial (no background sync)
Mobile SafariFull with install prompt

Best Practices

  • Service workers require HTTPS in production (localhost is exempt for development).
  • Use the network_first strategy for dynamic content and cache_first for static assets.
  • Validate data in the sync queue before sending to the server.
  • Consider authentication token expiry when designing offline flows.
  • Test offline behavior in Chrome DevTools (Application > Service Workers > Offline).