Skip to content
qt

Quickstart

Up and running in 5 minutes

The shortest path from zero to tag-based invalidation in a TanStack Start app. Install, define tags, tag one query, invalidate once — done.

Prerequisites

TanStack Start app@tanstack/react-query v5+@tanstack/react-router v1+react 19+
1

Install

Add the package and its peer dependency. keyv provides the in-process tag registry.

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 once. Parent nodes are group tags — invalidating a parent refreshes all children.

src/lib/tags.tsTS
// src/lib/tags.ts
import { defineTags } from "@tanstack-tools/query-tags"

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

Tag one query

Wrap any useQuery call with withTags(). The runtime picks up the tags automatically — no other changes.

// In your component or query factory
import { useQuery } from "@tanstack/react-query"
import { withTags } from "@tanstack-tools/query-tags"
import { appTags } from "./tags"

export function useTodos() {
  return useQuery(
    withTags(
      { queryKey: ["todos"], queryFn: fetchTodos },
      [appTags.todos.list()],
    ),
  )
}

You can also use meta: { tags: [...] } inline. withTags is cleaner for shared query factories.

4

Create the server system

Instantiate the invalidation system on the server once. Keep this import server-only.

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

export const tagInvalidation = createTagInvalidationSystem()

This file imports from @tanstack-tools/query-tags/server which uses Node.js APIs. Name it *.server.ts or keep it behind a server function — never import it in client code.

5

Mount the SSE route

Add a catch-all API route that handles both the SSE stream (GET) and control endpoint (POST).

src/routes/api.invalidator.$.tsTS
// 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 },
  },
})
6

Wrap the provider

Add TagInvalidationProvider high up in your tree — typically in your root provider file. It opens the SSE connection.

// 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({
  basePath: "/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}
      >
        {children}
      </TagInvalidationProvider>
    </QueryClientProvider>
  )
}
7

Invalidate from the server

Call invalidateTags() in any server function after a mutation. Every tagged query refetches automatically.

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

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

    // One call refreshes every query tagged with todos.*
    await tagInvalidation.invalidateTags({
      tags: [appTags.todos()],
    })
  })

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

That's it

From this point, every query tagged with appTags.todos.* refetches automatically whenever a server function calls invalidateTags. Every browser tab. Every active component. No invalidateQueries needed on the client.

Next step

Full integration guide

Redis for multi-instance, scope isolation, SPA + backend setup, and all framework adapters.

Read the guide →

See it live

Interactive demos

Live examples with real SSE — tagged queries, group invalidation, multi-tab sync, and more.

Explore demos →