SwoffSwoff

Tag System

Auto-tags, glob patterns, cascading invalidation, cross-tab, and framework adapters.

If you're coming from TanStack Query: invalidateByTag("notes") is Swoff's equivalent of queryClient.invalidateQueries({ queryKey: ["notes"] }). The difference: Swoff auto-generates tags from URL patterns, supports glob matching, cascading invalidation, and works cross-tab — no query key management needed. See the GraphQL & Push architecture.

No-bundler projects do not generate cache/invalidate.ts. Use the X-SW-Cache-Tags header approach (section 1b below) to set tags via native fetch or HTMX headers, or use the window.swoff API: swoff.invalidateByTag(), swoff.invalidateByTags(), swoff.invalidateMatching(), swoff.getUrlsForTag(), swoff.getTagsForUrl() — all available after including swoff-api.bundle.js.

Preconditions

  • Swoff initialized with fetchWithCache in use
  • Service worker controlling the page

Status

Already on by default. Tag invalidation is always active when the tagInvalidation object is present in your config. Every fetchWithCache call auto-generates tags from the request URL.

Generated files

FileWhat it doesImport in your code?
swoff/cache/tags.{ts|js}generateTags(), generateTagsFromMethod() — URL-based tag generationUsually not needed (auto-invoked by SW)
swoff/cache/invalidate.{ts|js}invalidateByTag(), invalidateByTags(), invalidateUrl(), invalidateByMethod(), invalidateMatching(), getUrlsForTag(), getTagsForUrl(), expandCascading()Yes — main invalidation API
swoff-api.bundle.jsSame invalidation API on window.swoff for no-bundler projectsNo (auto-initializes)

Usage — comprehensive examples

1a. Auto-tags from URL (via fetchWithCache)

fetchWithCache auto-tags every request. Tags are derived from URL segments with prefix skipping:

GET /api/notes          → tags: ["notes"]
GET /api/notes/123      → tags: ["notes", "note:123"]
GET /api/users/456/posts → tags: ["users", "user:456", "posts"]

The SW stores these tags alongside the cached response. Invalidate with:

Bundler:

import { invalidateByTag } from "./swoff/cache/invalidate";

// After creating a new note, refresh the notes list
await invalidateByTag("notes");

// After updating a specific note
await invalidateByTag("note:123");

No-bundler:

<script src="/swoff/swoff-api.bundle.js"></script>
<script>
  await swoff.invalidateByTag("notes");
  await swoff.invalidateByTag("note:123");
</script>

1b. Set tags from any HTTP client (X-SW-Cache-Tags header)

No swoff import needed — set the X-SW-Cache-Tags header on any request and the SW will tag the cached response:

// Native fetch — tags set via header
fetch("/api/notes", {
  headers: { "X-SW-Cache-Tags": "notes, dashboard" },
});
<!-- HTMX — tags via hx-headers -->
<button
  hx-get="/api/notes"
  hx-headers='{"X-SW-Cache-Tags": "notes, dashboard"}'
>
  Load Notes
</button>

This works with any HTTP client — the SW reads the header and stores the tags alongside the cached response. Tags set via header are merged with auto-generated tags from the URL.

Custom patterns

Override auto-tag generation with URL pattern → tag mappings. Added to features.tagInvalidation.patterns:

{
  "tagInvalidation": {
    "patterns": {
      "/api/(posts|users)/:id": ["posts", "users"],
      "/api/posts/:postId/comments": ["comments", "posts"]
    }
  }
}

Now:

GET /api/posts/123       → tags: ["posts", "users"]
GET /api/posts/456/comments → tags: ["comments", "posts"]

Cascading invalidation

Invalidating one tag can cascade to dependent tags:

{
  "tagInvalidation": {
    "cascading": {
      "notes": ["dashboard", "stats"],
      "users": ["dashboard", "permissions"]
    }
  }
}
await invalidateByTag("notes");
// Invalidates: "notes", "dashboard", "stats"

Glob pattern matching

Invalidate all cache entries whose URL matches a glob pattern. This operates on cached URLs, not tag names:

Bundler:

import { invalidateMatching } from "./swoff/cache/invalidate";

// Invalidate all /api/notes/* cached responses
await invalidateMatching("/api/notes/*");

// Invalidate all cached responses under /api/**
await invalidateMatching("/api/**");

No-bundler:

<script>
  await swoff.invalidateMatching("/api/notes/*");
  await swoff.invalidateMatching("/api/**");
</script>

Manual invalidation endpoint

Wire up a backend endpoint to push invalidation. Your server sends a request to a route the SW intercepts:

import { invalidateByTags } from "./swoff/cache/invalidate";

// POST /api/revalidate — called by your backend
app.post("/api/revalidate", async (req, res) => {
  // req.body = { tags: ["notes", "users"] }
  // Swoff's SW intercepts this and calls invalidateByTags
  res.json({ ok: true });
});

// Or manually from browser code:
await invalidateByTags(["notes", "users:*"]);

No-bundler:

<script>
  await swoff.invalidateByTags(["notes", "users:*"]);
</script>

Cross-tab invalidation

invalidateByTag works across all tabs — the SW receives the message and broadcasts a cache-invalidation event to all clients:

No manual coordination needed. Works out of the box.

Framework adapters

The cache-invalidated event drives the useSwoffFetch adapter (see the Data Fetching guide). When tags are invalidated, your adapter must listen for this event and refetch affected data.

Event:

EventDetail shapePurpose
cache-invalidated{ tags }Cache entries invalidated — refetch any data whose URL tags intersect

If your framework already has a useSwoffFetch adapter (React, Vue, Svelte), the CLI generates it into swoff/adapters/ — it listens to cache-invalidated automatically. To create adapters for any other framework, see the Blueprint for the event reference and adapter patterns.

For introspection (debugging which URLs are tagged), use getUrlsForTag(tag) or swoff.getUrlsForTag(tag) — see the Introspection section above.

Introspection

Bundler:

import { getUrlsForTag, getTagsForUrl } from "./swoff/cache/invalidate";

// Debug: what URLs are cached under a tag?
const urls = await getUrlsForTag("notes");
// [{ url: "/api/notes", actualUrl: "https://..." }]

// Debug: what tags are associated with a URL?
const tags = await getTagsForUrl("/api/notes/123");
// ["notes", "note:123"]

No-bundler:

<script>
  const urls = await swoff.getUrlsForTag("notes");
  const tags = await swoff.getTagsForUrl("/api/notes/123");
</script>

Config

{
  "features": {
    "tagInvalidation": {
      "debounceMs": 0,
      "skipPrefixes": ["api", "v1", "v2", "v3", "rest", "graphql", "gql"],
      "patterns": {},
      "singularization": {},
      "cascading": {}
    }
  }
}
  • Tag invalidation is always active when the tagInvalidation object is present — no enabled toggle needed.
  • debounceMs — coalesce rapid invalidations (e.g., 500ms debounce)
  • skipPrefixes — URL path segments to ignore during auto-tagging
  • patterns — URL pattern → tag mappings (regex path params)
  • singularization — custom plural → singular mapping (e.g. "people" → "person")
  • cascading — tag → dependent tags map

On this page