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 ofqueryClient.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 theX-SW-Cache-Tagsheader approach (section 1b below) to set tags via native fetch or HTMX headers, or use thewindow.swoffAPI:swoff.invalidateByTag(),swoff.invalidateByTags(),swoff.invalidateMatching(),swoff.getUrlsForTag(),swoff.getTagsForUrl()— all available after includingswoff-api.bundle.js.
Preconditions
- Swoff initialized with
fetchWithCachein 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
| File | What it does | Import in your code? |
|---|---|---|
swoff/cache/tags.{ts|js} | generateTags(), generateTagsFromMethod() — URL-based tag generation | Usually 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.js | Same invalidation API on window.swoff for no-bundler projects | No (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:
| Event | Detail shape | Purpose |
|---|---|---|
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
tagInvalidationobject is present — noenabledtoggle needed. debounceMs— coalesce rapid invalidations (e.g., 500ms debounce)skipPrefixes— URL path segments to ignore during auto-taggingpatterns— URL pattern → tag mappings (regex path params)singularization— custom plural → singular mapping (e.g."people" → "person")cascading— tag → dependent tags map
Related
- Library Comparison — cross-tab sync in Tier 1 feature matrix
- Optimistic updates design rationale