Swoff Swoff
Reference Example

E-commerce App

Shopping cart demonstrating offline storage, queued checkout, and authenticated user.

E-commerce Reference

A shopping cart demonstrating offline reads, cart persistence with IndexedDB, queued checkout, and auth integration.

What It Demonstrates

  • ✅ Authenticated user with cached profile
  • ✅ Product catalog cached for offline browsing
  • ✅ Cart persists offline (IndexedDB)
  • ✅ Checkout queued when offline, synced when online
  • ✅ Order history cached

Architecture

┌─────────────────────────────────────────────────┐
│                 Client (Browser)                 │
├─────────────────────────────────────────────────┤
│  Service Worker                                │
│  ├── Product cache (Cache API)                 │
│  ├── Cart cache (Cache API)                    │
│  └── Order queue (IndexedDB)                   │
├─────────────────────────────────────────────────┤
│  IndexedDB                                      │
│  ├── user (auth profile)                        │
│  ├── products (cached catalog)                 │
│  ├── cart (local cart)                          │
│  └── orders (synced orders)                    │
└─────────────────────────────────────────────────┘
              ↓ (when online)
┌─────────────────────────────────────────────────┐
│  Your Backend (orders API)                      │
└─────────────────────────────────────────────────┘

Key Files

Cart Storage (IndexedDB)

// lib/storage/cart.ts
const DB_NAME = "ecommerce";
const CART_STORE = "cart";

export async function addToCart(product, quantity = 1) {
  const db = await openDB(DB_NAME, 1);
  const tx = db.transaction(CART_STORE, "readwrite");
  const store = tx.objectStore(CART_STORE);

  const existing = await getCartItem(product.id);
  if (existing) {
    existing.quantity += quantity;
    store.put(existing);
  } else {
    store.put({
      id: product.id,
      title: product.title,
      price: product.price,
      image: product.image,
      quantity,
      addedAt: Date.now(),
    });
  }
}

export async function getCart() {
  const db = await openDB(DB_NAME);
  return new Promise((resolve, reject) => {
    const tx = db.transaction(CART_STORE, "readonly");
    const request = tx.objectStore(CART_STORE).getAll();
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

export async function getCartTotal() {
  const cart = await getCart();
  return cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

Cart Page (Offline-Ready)

// pages/Cart.tsx
import { useState, useEffect } from "react";
import { useNetworkStatus } from "./swoff/hooks/useNetworkStatus";
import { useMutationQueue } from "./swoff/hooks/useMutationQueue";
import { getCart, getCartTotal } from "../lib/storage/cart";

export function CartPage() {
  const [cart, setCart] = useState([]);
  const [total, setTotal] = useState(0);
  const isOnline = useNetworkStatus();
  const { pendingCount } = useMutationQueue();

  useEffect(() => {
    loadCart();
  }, []);

  async function loadCart() {
    const items = await getCart();
    const cartTotal = await getCartTotal();
    setCart(items);
    setTotal(cartTotal);
  }

  async function checkout() {
    const order = {
      items: cart,
      total,
      createdAt: Date.now(),
    };

    // Queue for offline sync
    await queueMutation({
      method: "POST",
      url: "/api/orders",
      body: order,
      tags: ["orders", "cart"],
      storeName: "orders",
      tempId: `temp-${Date.now()}`,
    });

    // Clear local cart
    await clearCart();
    loadCart();
  }

  return (
    <div>
      <h1>Shopping Cart</h1>

      {!isOnline && (
        <div role="alert" aria-live="polite" className="offline-banner">
          You are offline. Your order will be placed when you reconnect.
        </div>
      )}

      {cart.length === 0 ? (
        <p>Your cart is empty</p>
      ) : (
        <ul>
          {cart.map((item) => (
            <li key={item.id}>
              {item.title} - ${item.price} x {item.quantity}
            </li>
          ))}
        </ul>
      )}

      <p>Total: ${total}</p>

      {pendingCount > 0 && (
        <div role="status" aria-live="polite">
          {pendingCount} order(s) pending...
        </div>
      )}

      <button onClick={checkout} disabled={cart.length === 0}>
        {isOnline ? "Place Order" : "Place Order (Offline)"}
      </button>
    </div>
  );
}

Product Catalog (Cached)

// pages/Products.tsx
import { fetchWithCache } from "../swoff/fetch-wrapper.js";

export async function getStaticProps() {
  // Products are cached by SW for offline
  const response = await fetchWithCache("/api/products", {
    tags: ["products"],
    staleWhileRevalidate: true,
  });
  const products = await response.json();

  return { props: { products } };
}

export function ProductsPage({ products }) {
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          <img src={product.image} alt={product.title} />
          <h3>{product.title}</h3>
          <p>${product.price}</p>
          <button onClick={() => addToCart(product)}>
            Add to Cart
          </button>
        </li>
      ))}
    </ul>
  );
}

Offline Behavior

User ActionOnlineOffline
Browse productsFetch from API, cacheServe from cache
Add to cartWrite to IDBWrite to IDB
View cartRead from IDBRead from IDB
CheckoutPOST to APIQueue to IDB
Order syncedShow confirmationShow "pending" badge
Back online-Queue auto-processes

What's Included

FeatureStatus
Product catalog cache
Cart persistence
Checkout queue
Auth integration
Order history
Offline indicator
Sync status

Next Steps

On this page