Skip to content
qt

Quick start

Up and running in 5 minutes

Seven steps from zero to real-time tag-based cache invalidation in your TanStack Start app.

1
2
3
4
5
6
7

Prerequisites

TanStack Start app@tanstack/react-query v5+@tanstack/react-router v1+Node.js 18+
1

Install

Add the plugin and its peer dependency. keyv is used for the in-process tag registry — you can swap it for Redis in production.

pnpm add @tanstack-tools/query-tags keyv

Peer deps: @tanstack/react-query >=5, @tanstack/react-router >=1, @tanstack/react-start >=1

2

Define Your Tags

Create a typed tag tree. Every leaf is a callable factory. Parent nodes are group tags — invalidating a parent also invalidates all its children.

// src/lib/tag-contract.ts
import { function defineTags<T extends ValidTagFactoryTree<T>>(factories: T): ResolvedTagTree<T>
Builds a typed, callable tag tree from a plain factory object. Each leaf becomes a tag factory stamped with its `TAG_PATH`; each branch becomes a callable `TagGroup` that expands to all descendant leaves when invoked without arguments.
@paramfactories - Nested object of tag factory functions.@returnsA fully-typed tag tree with autocomplete and compile-time safety.@example```ts const appTags = defineTags({ todos: { list: () => ["todos"], byId: (id: string) => ["todo", id] } }); appTags.todos.list(); // leaf tag value appTags.todos(); // TagGroup expanding to all todo leaves ```
defineTags
} from "@tanstack-tools/query-tags"
export const
const appTags: ResolvedTagTree<{
    todos: {
        list: () => string[];
        summary: () => string[];
        byId: (id: string) => string[];
    };
    notes: {
        list: () => string[];
    };
}>
appTags
=
defineTags<{
    todos: {
        list: () => string[];
        summary: () => string[];
        byId: (id: string) => string[];
    };
    notes: {
        list: () => string[];
    };
}>(factories: {
    todos: {
        list: () => string[];
        summary: () => string[];
        byId: (id: string) => string[];
    };
    notes: {
        list: () => string[];
    };
}): ResolvedTagTree<{
    todos: {
        list: () => string[];
        summary: () => string[];
        byId: (id: string) => string[];
    };
    notes: {
        list: () => string[];
    };
}>
Builds a typed, callable tag tree from a plain factory object. Each leaf becomes a tag factory stamped with its `TAG_PATH`; each branch becomes a callable `TagGroup` that expands to all descendant leaves when invoked without arguments.
@paramfactories - Nested object of tag factory functions.@returnsA fully-typed tag tree with autocomplete and compile-time safety.@example```ts const appTags = defineTags({ todos: { list: () => ["todos"], byId: (id: string) => ["todo", id] } }); appTags.todos.list(); // leaf tag value appTags.todos(); // TagGroup expanding to all todo leaves ```
defineTags
({
todos: {
    list: () => string[];
    summary: () => string[];
    byId: (id: string) => string[];
}
todos
: {
list: () => string[]list: () => ["todos"], summary: () => string[]summary: () => ["todos", "summary"], byId: (id: string) => string[]byId: (id: stringid: string) => ["todo", id: stringid], },
notes: {
    list: () => string[];
}
notes
: {
list: () => string[]list: () => ["notes"], }, }) // Export the type for use in server functions export type
type AppTags = (() => TagGroup) & {
    todos: TagNode<{
        list: () => string[];
        summary: () => string[];
        byId: (id: string) => string[];
    }>;
    notes: TagNode<{
        list: () => string[];
    }>;
}
AppTags
= typeof
const appTags: ResolvedTagTree<{
    todos: {
        list: () => string[];
        summary: () => string[];
        byId: (id: string) => string[];
    };
    notes: {
        list: () => string[];
    };
}>
appTags

TypeScript infers the full tree. You get autocomplete on appTags.todos.byId('…') everywhere.

3

Create the Server System

Instantiate the tag invalidation system once on the server. This creates the SSE stream manager and the tag registry.

// src/lib/tag-invalidation.ts  (server-only)
import { createTagInvalidationSystem } from "@tanstack-tools/query-tags/server"

// Zero-config: in-memory store, global scope
export const tagInvalidation = createTagInvalidationSystem()

// Or with custom options:
// createTagInvalidationSystem({
//   store: new Keyv({ store: new KeyvRedis(…) }),
//   resolveScope: (ctx) => ({ kind: "user", id: ctx.userId }),
// })

This module must only be imported in server-side code (server functions, API routes).

4

Mount the SSE API Handler

Add a catch-all API route that serves the Server-Sent Events stream. Clients connect here to receive invalidation events.

// src/routes/api.invalidator.$.ts
import { createFileRoute } from "@tanstack/react-router"
import { tagInvalidation } from "#/lib/tag-invalidation"

const handler = tagInvalidation.createAPIHandler({
  basePath: "/api/invalidator",
})

async function handle({ request }: { request: Request }) {
  return handler(request)
}

export const Route = createFileRoute("/api/invalidator/$")({
  server: {
    handlers: {
      GET: handle,
      POST: handle,
    },
  },
})

The basePath must match the apiBasePath (or client sseBasePath) in TagInvalidationProvider (default: /api/invalidator). Both GET (SSE stream) and POST (control endpoints) are handled.

5

Wrap the Provider

Add TagInvalidationProvider high up in your component tree — typically in __root.tsx. It establishes the SSE connection and scans active queries for matching tags on invalidation.

// src/integrations/tanstack-query/root-provider.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { useRouter } from "@tanstack/react-router"
import { TagInvalidationProvider } from "@tanstack-tools/query-tags/react"
import { createInvalidatorClient } from "@tanstack-tools/query-tags/client"

const invalidatorClient = createInvalidatorClient({
  sseBasePath: "/api/invalidator",
})

export default function TanStackQueryProvider({
  children,
}: {
  children: React.ReactNode
}) {
  const queryClient = /* your QueryClient instance */
  const router = useRouter()

  return (
    <QueryClientProvider client={queryClient}>
      <TagInvalidationProvider
        client={invalidatorClient}
        queryClient={queryClient}
        router={router}
        // scope={{ kind: "user", id: currentUser.id }}  // optional
      >
        {children}
      </TagInvalidationProvider>
    </QueryClientProvider>
  )
}

The provider needs queryClient and router props. The client prop connects it to the SSE endpoint. Import from @tanstack-tools/query-tags/react (not the root).

6

Tag Your Queries

Add meta.tags to any useQuery call you want to make reactive. The provider picks up the tags automatically — no other changes needed.

// In any component or query factory
import { useQuery } from "@tanstack/react-query"
import { appTags } from "#/lib/tag-contract"

// Option A: inline meta
export function useTodos() {
  return useQuery({
    queryKey: ["todos"],
    queryFn:  fetchTodos,
    meta: { tags: [appTags.todos.list()] },
  })
}

// Option B: withTags() helper
import { withTags } from "@tanstack-tools/query-tags"

export const todosQuery = withTags(
  { queryKey: ["todos"], queryFn: fetchTodos },
  [appTags.todos.list()]
)

You can tag multiple queries with the same tag — all will refresh together.

7

Invalidate from the Server

Call tagInvalidation.invalidateTags() in any server function or API handler. The SSE event is broadcast and every matching query refetches.

// src/lib/server-functions.ts
import { createServerFn } from "@tanstack/react-start"
import { tagInvalidation } from "./tag-invalidation"
import { appTags } from "./tag-contract"
import { z } from "zod"

export const addTodoServerFn = createServerFn()
  .inputValidator(z.object({ name: z.string().min(1) }))
  .handler(async ({ data }) => {
    await db.insert(todos).values({ name: data.name })

    // Invalidate ALL todo queries in a single call
    await tagInvalidation.invalidateTags({
      tags: [appTags.todos()],
    })
  })

appTags.todos() is the group tag — it expands to all children: list, summary, byId(*), etc.

Demos

See it working end-to-end

The live demos cover everything from basic tagged queries to multi-tab sync, scopes, and advanced patterns.

Explore demos →