Skip to content
qt

Documentation

API Reference

Every entry point, function, hook, and option in @tanstack-tools/query-tags — documented with copy-pasteable examples.

Server-onlyNode.js / server runtime
Client-onlyBrowser / React
UniversalAny runtime
IsomorphicShared primitives

Root — shared primitives

Isomorphic@tanstack-tools/query-tags

Safe to import anywhere — no Node.js or browser APIs. Provides tag definition, normalization, introspection helpers, and type augmentations.

defineTags

Builds a typed, callable tag tree from a factory object. Each leaf factory is stamped with its path for group matching; each branch node is callable as a TagGroup that expands to all descendant zero-arg leaves.

import { defineTags } from "@tanstack-tools/query-tags";

const appTags = defineTags({
  todos: {
    list: () => ["todos"],
    byId: (id: string) => ["todo", id],
  },
  users: {
    list: () => ["users"],
    byId: (id: string) => ["user", id],
  },
});

// Leaf tags
appTags.todos.list()          // ["todos"]
appTags.todos.byId("abc")     // ["todo", "abc"]

// Group — expands all zero-arg leaves when passed to invalidateTags
appTags.todos()               // TagGroup { leaves: [["todos"]] }
appTags()                     // TagGroup { leaves: [["todos"], ["users"]] }
Do not pass the factory object with as const — it widens leaf return types to readonly arrays which are not assignable to TagValue. Define inline or use a plain variable.

withTags

Merges tags into query options, setting meta.tags and deriving a stable queryKey from the tag values when no explicit key is provided. This is the canonical zero-boilerplate form; the meta.tags form is fully equivalent.

import { withTags } from "@tanstack-tools/query-tags";
import { useQuery } from "@tanstack/react-query";

// Form 1 — wrap existing options (canonical)
const query = useQuery(
  withTags({ queryFn: fetchTodos }, [appTags.todos.list()]),
);
// → { queryKey: ['["todos"]'], meta: { tags: [["todos"]] }, queryFn: fetchTodos }

// Form 2 — meta.tags directly (equivalent, no helper)
const query2 = useQuery({
  queryKey: ["todos"],
  queryFn: fetchTodos,
  meta: { tags: [appTags.todos.list()] },
});

Introspection helpers

Inspect the QueryClient cache without firing any invalidation. Useful for debugging and test assertions.

import {
  matchTags,
  findQueriesByTag,
  listAllTags,
  EVERY_INVALIDATION_TAG,
} from "@tanstack-tools/query-tags";

// Which queries would be invalidated by these tags?
const hits = matchTags({ queryClient, tags: [appTags.todos.list()] });
// → [{ queryKey: ["todos"], queryHash: '["todos"]' }, …]

// Queries tagged with a single specific tag
const queries = findQueriesByTag({ queryClient, tag: appTags.todos.list() });

// All tags currently registered in the cache
const allTags = listAllTags({ queryClient });
// → ['"__every_invalidation__"', '["todos"]', '["users"]']

// Tag that matches on every invalidation event
meta: { tags: [EVERY_INVALIDATION_TAG] }
NameTypeDefaultDescription
matchTags({ queryClient, tags }) => Array<{ queryKey, queryHash }>Returns every cached query that would be invalidated by the given tags.
findQueriesByTag({ queryClient, tag }) => Array<{ queryKey, queryHash }>Returns queries tagged with a single specific tag.
listAllTags({ queryClient }) => string[]Enumerates all distinct normalized tags across all cached queries.
EVERY_INVALIDATION_TAGstring — "__every_invalidation__"Attach to a query's meta.tags to refetch it on every invalidation event.

Server — tag invalidation system

Server-only@tanstack-tools/query-tags/server

The single entry point for all server-side invalidation: SSE streaming, control API, batching, scopes, and broadcast. Uses node:events and keyv — do not import in client bundles.

import { createTagInvalidationSystem } from "@tanstack-tools/query-tags/server";

// Minimal — defaults are sensible
const tagInvalidation = createTagInvalidationSystem();

// Full options
const tagInvalidation = createTagInvalidationSystem<MyContext>({
  batchWindow: 50,         // ms — merge rapid-fire events (default: 50)
  heartbeatInterval: 30000,// ms — keep proxies alive (default: 30 000)
  eventLogSize: 1000,      // ring-buffer per scope (default: 1 000)
  maxConnections: 10000,   // global SSE cap (default: 10 000)
  maxConnectionsPerScope: 1000, // per-scope SSE cap (default: 1 000)
  maxScopeKeyLength: 128,  // max scope key chars (default: 128)
  broadcast: myBroadcast,  // multi-instance adapter (default: in-process)
  resolveScope: (ctx) => ({ kind: "user", id: ctx.userId }),
  rateLimit: false,        // disable rate limiting (or pass tiers)
  resolveClientIp: (req) => req.headers.get("CF-Connecting-IP") ?? "",
  onDevtoolsEvent: (event) => devtoolsSink.emit(event),
});

// Invalidate from a server action
await tagInvalidation.invalidateTags({ tags: [appTags.todos.list()] });

createTagInvalidationSystem options

NameTypeDefaultDescription
batchWindownumber50Merge invalidations within this window (ms) into one SSE event. Set 0 to disable.
heartbeatIntervalnumber30000SSE heartbeat comment interval (ms) to prevent proxy idle timeouts. Set 0 to disable.
eventLogSizenumber1000Per-scope ring-buffer size for Last-Event-ID replay on reconnect.
maxConnectionsnumber10000Total SSE connection cap across all scopes. Set 0 to disable.
maxConnectionsPerScopenumber1000Per-scope SSE connection cap. Set 0 to disable.
maxScopeKeyLengthnumber128Maximum character length for scope key strings.
broadcastBroadcastAdaptercreateLocalBroadcast()Multi-instance broadcast adapter. Pass createRedisBroadcast() or createWebBroadcast() for multi-process deployments.
resolveScope(context) => InvalidationScope() => { kind: 'global' }Derive per-request scope from server context. Used by oRPC/tRPC middleware.
rateLimitfalse | RateLimiterTier | RateLimiterTier[]tiered: 30/min, 300/10min, 1000/hrRate limiting for control API POST endpoints. Pass false to disable.
resolveClientIp(request: Request) => stringX-Forwarded-For headerExtract client IP for rate limiting.
onDevtoolsEvent(event: DevtoolsEvent) => voidundefinedReceive invalidation and connection events for the DevTools adapter.

Return value

NameTypeDefaultDescription
invalidateTags({ tags, context?, scope? }) => Promise<InvalidationMessage | null>Fire tag invalidation. Resolves after the batch window with the emitted message, or null if no clients are connected.
createAPIHandler({ basePath?, authenticate? }?) => (req: Request) => Promise<Response>Returns a fetch-standard handler for the SSE + control API. Used internally by all framework adapters.
getStatus() => InvalidationStatusCurrent system state: paused, disabledTags, connectedClients, recentInvalidations.
getInvalidationHistory() => InvalidationHistoryEntry[]Rolling log of all invalidation events.
disableTags / enableTags(tags: string[]) => voidSuppress or re-enable specific tags without restarting the system.
pauseAll / resumeAll() => voidPause or resume the entire invalidation pipeline.
dispose() => voidClose all SSE connections and release resources.

React — provider and hooks

Client-only@tanstack-tools/query-tags/react

React context provider and all hooks. The provider opens the SSE connection and drives cache invalidation. Hooks provide access to status, history, and control.

TagInvalidationProvider

import { TagInvalidationProvider } from "@tanstack-tools/query-tags/react";
import { createInvalidatorClient } from "@tanstack-tools/query-tags/client";

const client = createInvalidatorClient();

// Minimal (required props only)
<TagInvalidationProvider queryClient={queryClient} router={router}>
  <App />
</TagInvalidationProvider>

// With all options
<TagInvalidationProvider
  queryClient={queryClient}
  router={router}
  client={client}           // enables status/history/control hooks
  scope={{ kind: "user", id: userId }}
  eventSourceUrl="/api/invalidator/sse"
  onDisconnect="invalidate-all"   // or "silent" | { strategy, tags? }
  onError={(err, ctx) => console.error(ctx, err)}
  dedupeWindow={50}               // ms client-side dedup (default: 50)
  conditionalInvalidation={true}  // skip fresh queries (default: false)
  tabLeader={true}                // WS1 tab leader election
>
  <App />
</TagInvalidationProvider>
NameTypeDefaultDescription
queryClientQueryClientRequired. The TanStack QueryClient to invalidate against.
routerRouterLikeRequired. The TanStack Router instance. Used to invalidate loader data.
clientInvalidatorClientundefinedEnables useInvalidationStatus, useInvalidationHistory, and useInvalidationControl hooks.
scopeInvalidationScope{ kind: 'global' }Which SSE scope this client subscribes to.
eventSourceUrlstring/api/invalidator/sseOverride the SSE endpoint URL.
onDisconnect"invalidate-all" | "silent" | object"invalidate-all"What to do on SSE disconnect. "invalidate-all" refetches everything; "silent" does nothing.
dedupeWindownumber50Client-side dedup window (ms). SSE messages within the window are merged before processing.
conditionalInvalidationboolean | ConditionalInvalidationOptionsfalseSkip refetching queries whose cached data is fresher than the invalidation timestamp.
tabLeaderboolean | TabLeaderOptionsfalseEnable tab leader election (WS1). Only one tab opens SSE; others receive via BroadcastChannel.

Hooks

import {
  useTagInvalidationRuntime,
  useTagInvalidationDebugState,
  useInvalidationClient,
  useInvalidationStatus,
  useInvalidationHistory,
  useInvalidationControl,
  useInvalidationActions,
  useOptimisticInvalidation,
  useOptimisticMutation,
} from "@tanstack-tools/query-tags/react";

// Debug state — connected, paused, event log
const { isConnected, isPaused, messages } = useTagInvalidationDebugState();

// Control panel (requires client prop on provider)
const { pauseAll, resumeAll, disableTags, enableTags, isPausing } =
  useInvalidationControl();

// Polling queries (require client prop)
const { data: status } = useInvalidationStatus({ refetchInterval: 5000 });
const { data: history } = useInvalidationHistory();

// Optimistic update with rollback
const [optimisticTodos, addOptimistic] = useOptimisticInvalidation(todos);
NameTypeDefaultDescription
useTagInvalidationRuntime() => runtimeRaw runtime — processMessage, getDebugState, subscribe. For advanced/devtools use.
useTagInvalidationDebugState() => DebugStateReactive debug state: isConnected, isPaused, messages, eventCount.
useHasInvalidationClient() => booleanWhether an InvalidatorClient was provided to the nearest provider.
useInvalidationClient() => InvalidatorClientReturns the client; throws if none was provided.
useInvalidationStatus({ refetchInterval? }) => QueryResult<InvalidationStatus>Polls server status. Requires client on provider.
useInvalidationHistory() => QueryResult<InvalidationHistoryEntry[]>Fetches history; auto-refetches on every invalidation via EVERY_INVALIDATION_TAG. Requires client.
useInvalidationControl() => ControlActionsMemoized actions: disableTags, enableTags, pauseAll, resumeAll, plus isPausing/isResuming flags.
useOptimisticInvalidation(state, reducer?) => [optimisticState, dispatch]Like useOptimistic — shows temporary state while a server action is in-flight.

Client — TanStack Start server functions

Isomorphic@tanstack-tools/query-tags/client

Control API client backed by TanStack Start server functions. Use this in TanStack Start apps. For plain SPAs with a separate backend, use /http-client instead.

import { createInvalidatorClient } from "@tanstack-tools/query-tags/client";

// TanStack Start — uses server functions under the hood
const client = createInvalidatorClient({ basePath: "/api/invalidator" });

// Attach to provider
<TagInvalidationProvider client={client} queryClient={queryClient} router={router}>
  <App />
</TagInvalidationProvider>
NameTypeDefaultDescription
createInvalidatorClient({ basePath? }) => InvalidatorClientCreates a typed client using TanStack Start server functions as the transport. basePath defaults to /api/invalidator.

HTTP Client — framework-agnostic

Isomorphic@tanstack-tools/query-tags/http-client

Fetch-based control API client. Use in SPA + separate backend setups (Express, Hono, Fastify, etc.). Also available as @tanstack-tools/query-tags/standalone.

import { createHTTPInvalidatorClient } from "@tanstack-tools/query-tags/http-client";
// alias: @tanstack-tools/query-tags/standalone

const client = createHTTPInvalidatorClient({
  basePath: "/api/invalidator",          // default
  fetch: customFetch,                    // optional
  headers: { Authorization: "Bearer …" }, // or () => ({ … })
});
NameTypeDefaultDescription
basePathstring"/api/invalidator"Base URL for all control API requests.
fetchtypeof fetchglobalThis.fetchCustom fetch implementation (e.g., for test mocking or cross-fetch).
headersRecord<string, string> | (() => Record<string, string>)undefinedExtra headers added to every request. Useful for auth tokens.

Framework adapters

Server-only@tanstack-tools/query-tags/adapters/*

Zero-dependency thin wrappers over createAPIHandler(). All adapters share the same CreateInvalidatorHandlerOptions interface.

// Hono
import { createHonoHandler } from "@tanstack-tools/query-tags/adapters/hono";
app.all("/api/invalidator/*", createHonoHandler(system));

// Express
import { createExpressHandler } from "@tanstack-tools/query-tags/adapters/express";
app.all("/api/invalidator/*", createExpressHandler(system));

// Fastify
import { createFastifyHandler } from "@tanstack-tools/query-tags/adapters/fastify";
fastify.all("/api/invalidator/*", createFastifyHandler(system));

// Elysia
import { createElysiaHandler } from "@tanstack-tools/query-tags/adapters/elysia";
app.all("/api/invalidator/*", createElysiaHandler(system));

// Koa
import { createKoaHandler } from "@tanstack-tools/query-tags/adapters/koa";
router.all("/api/invalidator/(.*)", createKoaHandler(system));

// Node.js HTTP / https.createServer
import { createNodeHandler } from "@tanstack-tools/query-tags/adapters/node";
http.createServer(createNodeHandler(system));

// Fetch-standard (Bun, Deno, Cloudflare Workers)
import { createFetchHandler } from "@tanstack-tools/query-tags/adapters/fetch";
export default { fetch: createFetchHandler(system) };

// Shared options (all adapters accept these)
const handler = createHonoHandler(system, {
  basePath: "/api/invalidator",  // strip prefix from incoming URLs
  authenticate: async (req) => {
    const token = req.headers.get("Authorization");
    return token === "Bearer secret"; // false → 401, Response → forwarded
  },
});
/adapters/hono

createHonoHandler(system, options?)

Structural duck typing — zero Hono peer dep

/adapters/express

createExpressHandler(system, options?)

Node.js IncomingMessage / ServerResponse bridge

/adapters/fastify

createFastifyHandler(system, options?)

Fastify request / reply bridge

/adapters/elysia

createElysiaHandler(system, options?)

Bun / web-standard fetch bridge

/adapters/koa

createKoaHandler(system, options?)

Koa context bridge

/adapters/node

createNodeHandler(system, options?)

Plain Node.js http.createServer

/adapters/fetch

createFetchHandler(system, options?)

Fetch-standard — Bun, Deno, Workers

CreateInvalidatorHandlerOptions (shared by all adapters)

NameTypeDefaultDescription
basePathstring"/api/invalidator"Strip this prefix from incoming request URLs before routing.
authenticate(request: Request) => boolean | Response | Promise<boolean | Response>undefinedReturn true to allow, false for 401, or a custom Response to forward.

Broadcast adapters — multi-instance

Server-only@tanstack-tools/query-tags/redis · /web-broadcast

By default the server uses an in-process EventEmitter. For multi-instance deployments, pass a broadcast adapter so SSE events reach clients connected to different server processes.

Redis — @tanstack-tools/query-tags/redis

import { createRedisBroadcast } from "@tanstack-tools/query-tags/redis";
import Redis from "ioredis";

const pub = new Redis();
const sub = new Redis();

const broadcast = createRedisBroadcast({
  channelPrefix: "query-tags:",      // default
  publish: (channel, msg) => pub.publish(channel, msg),
  subscribe: (channel, handler) => {
    sub.subscribe(channel);
    sub.on("message", (ch, msg) => ch === channel && handler(ch, msg));
    return () => sub.unsubscribe(channel);
  },
});

const system = createTagInvalidationSystem({ broadcast });

Web Broadcast — @tanstack-tools/query-tags/web-broadcast

In-process adapter using Map + Set. Works on all runtimes without node:events. Equivalent to the default createLocalBroadcast() (also re-exported from /server) but safe on edge and non-Node runtimes.

import { createWebBroadcast } from "@tanstack-tools/query-tags/web-broadcast";
// also re-exported from @tanstack-tools/query-tags/server

// In-process — works on all JS runtimes (Node, Bun, Deno, Workers)
// Equivalent to createLocalBroadcast() but without node:events dependency
const broadcast = createWebBroadcast();
const system = createTagInvalidationSystem({ broadcast });
/broadcast re-exports the BroadcastAdapter interface if you need to implement your own adapter (e.g., NATS, Kafka).

oRPC middleware

Server-only@tanstack-tools/query-tags/orpc

Middleware adapter for oRPC procedures. Injects context.invalidateTags into every procedure and automatically resolves scope from request context.

import { createTagsMiddleware } from "@tanstack-tools/query-tags/orpc";

const tagMiddleware = createTagsMiddleware({ system: tagInvalidation });

// Attach to any oRPC procedure
export const updateTodo = base
  .use(tagMiddleware)
  .handler(async ({ input, context }) => {
    const todo = await db.todos.update(input);
    context.invalidateTags([appTags.todos.byId(input.id)]);
    return todo;
  });

tRPC middleware

Server-only@tanstack-tools/query-tags/trpc

Middleware adapter for tRPC procedures. Injects ctx.invalidateTags into every procedure.

import { createTagsMiddleware } from "@tanstack-tools/query-tags/trpc";

const withTags = createTagsMiddleware({ system: tagInvalidation });

export const updateTodo = t.procedure
  .use(withTags)
  .mutation(async ({ input, ctx }) => {
    const todo = await db.todos.update(input);
    ctx.invalidateTags([appTags.todos.byId(input.id)]);
    return todo;
  });

Testing utilities

Test-only@tanstack-tools/query-tags/testing

Four helpers covering the full test surface: server assertions, client mocks, provider wrappers, and invalidation simulation. Requires vi.fn from Vitest.

import { vi } from "vitest";
import {
  createMockInvalidationSystem,
  createMockInvalidatorClient,
  createTestProvider,
  simulateInvalidation,
} from "@tanstack-tools/query-tags/testing";

// 1. Server-side assertions
const system = createMockInvalidationSystem();
await system.invalidateTags({ tags: [appTags.todos.list()] });
system.expectTagsInvalidated(['"todos"']);  // normalized string
system.expectTagsNotInvalidated(['"users"']);
system.getInvalidationCount('"todos"');     // → 1
system.reset();

// 2. Mock client for provider
const client = createMockInvalidatorClient({ vi });
client.mocks.getStatus.mockResolvedValue({ paused: true, … });

// 3. Test provider wrapper
const { TestProvider, queryClient, runtime } = createTestProvider({ client });
render(<MyComponent />, { wrapper: TestProvider });

// 4. Trigger invalidation without SSE
await simulateInvalidation(runtime, {
  tags: [appTags.todos.list()],
  scopeKey: "global",
});
NameTypeDefaultDescription
createMockInvalidationSystem() => MockSystemIn-memory mock with expectTagsInvalidated(), expectTagsNotInvalidated(), getInvalidationCount(), and reset().
createMockInvalidatorClient({ vi? }) => MockClientMock client where every method is a vi.fn() spy with sensible defaults. Exposes .mocks and .reset().
createTestProvider({ client?, scope? }) => { TestProvider, queryClient, router, runtime }Pre-configured provider wrapper for @testing-library/react render().
simulateInvalidation(runtime, { tags, scopeKey?, groupPaths? }) => Promise<void>Feeds an InvalidationMessage directly to the runtime without an SSE connection.

DevTools

Client-only@tanstack-tools/query-tags/devtools

Full observability panel: SSE event log, tag tree, query matching, connection health. Integrates as a TanStack Query DevTools plugin or runs standalone.

import {
  QueryTagsDevtoolsPanel,
  QueryTagsDevtools,
  queryTagsPlugin,
  useDevtoolsStore,
  createDevtoolsAdapter,
} from "@tanstack-tools/query-tags/devtools";

// As a TanStack Query DevTools plugin
<TanStackQueryDevtools plugins={[queryTagsPlugin]} />

// As a standalone floating widget
<QueryTagsDevtools position="bottom-right" initialOpen={false} />

// As an embedded panel
<QueryTagsDevtoolsPanel theme="dark" />

// Access store state programmatically
const { debugState, setActiveTab, setTheme } = useDevtoolsStore();
NameTypeDefaultDescription
queryTagsPluginTanStackDevtoolsPluginDrop-in plugin for <TanStackQueryDevtools plugins={[queryTagsPlugin]} />.
QueryTagsDevtoolsReact componentFloating draggable widget. Props: position ('bottom-right' default), initialOpen (false default).
QueryTagsDevtoolsPanelReact componentEmbedded panel for custom layouts. Props: theme ('light' | 'dark').
useDevtoolsStore() => DevtoolsStoreAccess active tab, selected tag, search query, and theme state programmatically.
createDevtoolsAdapter() => EventClientAdapterAdapter that bridges the server onDevtoolsEvent callback to the TanStack DevTools event bus.
For full setup instructions including plugin registration and standalone usage, see the DevTools Guide.

Core types

NameTypeDefaultDescription
InvalidationScope{ kind: "global" } | { kind: "user"; id: string } | { kind: "tenant"; id: string } | { kind: "custom"; key: string }Discriminated union for SSE channel routing. Global is the default.
TagInputTagValue | TagGroupAnything you can pass to invalidateTags() or meta.tags. TagGroups are expanded to their leaf values.
TagValuestring | number | boolean | null | TagValue[] | { [key: string]: TagValue }Any JSON-serializable value. Normalized to a stable string for matching.
TagGroup{ [TAG_GROUP]: true; leaves: TagValue[]; path: string }Returned by calling a branch node in the defineTags tree, e.g. appTags.todos().
InvalidatorClientinterfaceShared interface implemented by both createInvalidatorClient (/client) and createHTTPInvalidatorClient (/http-client). Methods: getStatus, getHistory, disableTags, enableTags, pause, resume, sseUrl.
BroadcastAdapterinterfaceInterface for custom multi-instance broadcast adapters. Methods: publish(scopeKey, message, eventId), subscribe(scopeKey, handler) → () => void.
EVERY_INVALIDATION_TAG"__every_invalidation__"Constant. Any query with this tag in meta.tags refetches on every invalidation event.

Entry point summary

Import pathEnvironmentMain exports
@tanstack-tools/query-tagsUniversaldefineTags, withTags, matchTags, findQueriesByTag, listAllTags, EVERY_INVALIDATION_TAG
.../serverServer-onlycreateTagInvalidationSystem, createLocalBroadcast, createWebBroadcast
.../reactClient-onlyTagInvalidationProvider, createTagInvalidationProvider, useTagInvalidationDebugState, useInvalidationControl, useInvalidationStatus, useInvalidationHistory, useOptimisticInvalidation
.../clientIsomorphiccreateInvalidatorClient (TanStack Start server fns)
.../http-clientIsomorphiccreateHTTPInvalidatorClient (fetch-based)
.../standaloneIsomorphicalias for /http-client
.../adapters/honoServer-onlycreateHonoHandler
.../adapters/expressServer-onlycreateExpressHandler
.../adapters/fastifyServer-onlycreateFastifyHandler
.../adapters/elysiaServer-onlycreateElysiaHandler
.../adapters/koaServer-onlycreateKoaHandler
.../adapters/nodeServer-onlycreateNodeHandler
.../adapters/fetchServer-onlycreateFetchHandler
.../redisServer-onlycreateRedisBroadcast
.../web-broadcastUniversalcreateWebBroadcast
.../broadcastServer-onlyBroadcastAdapter interface, createLocalBroadcast
.../orpcServer-onlycreateTagsMiddleware (oRPC)
.../trpcServer-onlycreateTagsMiddleware (tRPC)
.../testingTest-onlycreateMockInvalidationSystem, createMockInvalidatorClient, createTestProvider, simulateInvalidation
.../devtoolsClient-onlyqueryTagsPlugin, QueryTagsDevtools, QueryTagsDevtoolsPanel, useDevtoolsStore, createDevtoolsAdapter