SwoffSwoff

GraphQL

Body-hash caching, operation-name auto-tags, and framework adapters for GraphQL.

If you're coming from Apollo Client: Swoff caches GraphQL responses by body hash (query + variables → SHA-256 digest), not by normalized entities. No schema introspection, no fragment matchers, no InMemoryCache config. Each query is a standalone cache entry. Mutations auto-invalidate by operation name tags. See the GraphQL & Push architecture.

Preconditions

  • Swoff initialized with data fetching enabled
  • A GraphQL endpoint (default: /graphql)

Status

Disabled by default. GraphQL support must be explicitly enabled.

Enable

npx @swoff/cli add graphql

Or set features.graphql.enabled: true and features.graphql.endpoints in swoff.config.json then regenerate.

Generated files

FileWhat it doesImport in your code?
swoff/graphql/index.tsfetchWithGql(), queryGql(), mutateGql()Yes
swoff-api.bundle.jsSame API on swoff.queryGql(), swoff.mutateGql() for no-bundlerNo (auto-initializes)

Usage

import { queryGql, mutateGql } from "./swoff/graphql";

// Query — cached by body hash (query + variables)
// Auto-tagged by operation name: "GetNotes"
const { data, fromCache } = await queryGql(`
  query GetNotes {
    notes { id title }
  }
`);

// Query with variables
const { data: todo } = await queryGql(
  `
  query GetTodo($id: ID!) {
    todo(id: $id) { id title }
  }
`,
  { id: "42" },
);

// Authenticated query
const { data } = await queryGql(
  `
  query Me { me { name } }
`,
  {},
  { auth: true },
);

// Mutation — auto-invalidates related tags by operation name
const { data: created } = await mutateGql(
  `
  mutation CreateNote($title: String!) {
    createNote(title: $title) { id }
  }
`,
  { title: "New note" },
);
// Auto-invalidates: ["notes", "note"] (derived from "CreateNote")

// Offline: mutations are auto-queued
const { data } = await mutateGql(
  `
  mutation CreateNote($title: String!) {
    createNote(title: $title) { id }
  }
`,
  { title: "Offline task" },
  { queueOffline: true },
);

// Custom tags override operation-name auto-tags
const { data } = await queryGql(
  `query GetNotes { notes { id } }`,
  {},
  {
    tags: ["custom-tag"],
  },
);

Cross-query cache invalidation

Auto-tags from operation names handle paired read/write operations (e.g. GetNotesCreateNote). But the same entity type often appears in multiple queries — a User in GetDashboard, GetTeam, and me. Op-name auto-tags only cover the top-level operation, not nested entity references.

This is a deliberate tradeoff vs normalized caches like Apollo Client:

Swoff (body-hash + auto-tags)Apollo Client (normalized store)
Cross-query auto-invalidationOperation-name based. UpdateUser invalidates user; GetDashboard (tagged dashboard) is NOT auto-invalidatedAutomatic — Apollo extracts __typename + id, updates all queries referencing that entity
Schema couplingNone — no __typename, no id field requiredRequires __typename in every response, consistent id field
Runtime size0 kB (generated code)~32 kB gzip
Cache persistenceCache API (disk, survives tab close)In-memory
Offline mutation replay✅ Auto-invalidates cache tags on replay❌ No SW-level offline support
Entity update granularityInvalidates entire cache entryPatches individual entity fields
Anonymous query support❌ No op name → no auto-tags, can't be tag-invalidated✅ Normalizes by entity regardless

For most apps, op-name auto-tags cover the common CRUD pairs. When you need cross-query invalidation, use explicit tags and invalidate:

// Tag both queries with the same tag
const dashboard = await queryGql(`
  query GetDashboard { user { id name } team { id } }
`, {}, { tags: ["user", "team"] });

const team = await queryGql(`
  query GetTeam { members { id name } }
`, {}, { tags: ["user", "team"] });

// Invalidate both with the same tag
await mutateGql(`
  mutation UpdateProfile($name: String!) { updateProfile(name: $name) { id } }
`, { name }, { invalidate: ["user", "team"] });

Anonymous queries (no operation name, e.g. { todos { id } }) produce no auto-tags and cannot be tag-invalidated. Always name your operations for caching to work.

No-bundler usage

Include swoff-api.bundle.js after client-injector.bundle.js:

<script src="/swoff/swoff-api.bundle.js"></script>
<script>
  // Query — cached by body hash
  const { data, fromCache } = await swoff.queryGql(`
    query GetNotes { notes { id title } }
  `);

  // Mutation — auto-invalidates related tags
  const { data: created } = await swoff.mutateGql(`
    mutation CreateNote($title: String!) {
      createNote(title: $title) { id }
    }
  `, { title: "New note" });

  // Offline: mutations are auto-queued when mutation queue is enabled
  const { data } = await swoff.mutateGql(`
    mutation CreateNote($title: String!) {
      createNote(title: $title) { id }
    }
  `, { title: "Offline task" }, { queueOffline: true });
</script>

Framework adapters

GraphQL uses the same useSwoffFetch adapter pattern described in the Data Fetching guide. When a mutation auto-invalidates by operation name, the SW dispatches cache-invalidated:

Events:

EventDetail shapePurpose
cache-invalidated{ tags }Mutation auto-invalidated by operation name — refetch affected queries
swoff:cache-updated{ url }Background re-fetch completed (optional, for fine-grained reactivity)

No GraphQL-specific adapter is needed — the same useSwoffFetch adapter handles both REST and GraphQL. Body-hash caching and operation-name auto-tags happen automatically in the SW. See the Blueprint for the event reference and adapter patterns.

For a dedicated GraphQL API with operation-name auto-tags, use the generated queryGql/mutateGql functions instead.

From any HTTP client (header-based)

GraphQL responses can be cached by the SW even without the generated queryGql wrapper. The SW detects GraphQL requests by the presence of a query field in the POST body and computes the body hash automatically:

# Any HTTP client — POST to GraphQL endpoint
POST /graphql
Content-Type: application/json
X-SW-Strategy: cache-first

{"query": "query GetNotes { notes { id title } }"}

No swoff import needed. The SW hashes the request body and caches the response. Tag invalidation works via X-SW-Cache-Tags header on the response, or by using the generated invalidateByTag API from swoff/cache/invalidate.

Multiple endpoints

import { queryGql } from "./swoff/graphql";

// Pass endpointIndex to choose which endpoint
const { data } = await queryGql(`query { ... }`, {}, {}, 1);
// Uses GQL_ENDPOINTS[1]

Customize

No generated files to edit. The wrapper lives at swoff/graphql/index.ts and is regenerated on each swoff generate — manual edits would be overwritten.

Config

{
  "features": {
    "graphql": {
      "enabled": true,
      "endpoints": ["/graphql"]
    }
  }
}
  • endpoints — array of GraphQL endpoint paths. Index 0 is the default. Use endpointIndex in queryGql/mutateGql to select others.

On this page