Capxul Docs
Guides

TanStack Query integration

How the SDK rides TanStack Query — the provider-owned QueryClient, the key namespace, automatic invalidation, manual invalidation, and what clears on sign-out and re-bootstrap.

Every @capxul/sdk-react hook is a thin binding over TanStack Query: query hooks return UseQueryResult, mutation hooks return UseMutationResult, and all reactivity — caching, refetching, invalidation — is TanStack's. This guide explains the pieces you interact with when your app shares the cache with the SDK.

Before you start

  • @tanstack/react-query is installed (it is a peer dependency — the quickstart installs it).
  • CapxulProvider is mounted; you do not mount a QueryClientProvider yourself unless you bring your own client (below).

The provider owns the QueryClient

CapxulProvider creates a QueryClient and renders its own QueryClientProvider, so the hooks work with zero TanStack setup. The defaults it creates:

new QueryClient({
  defaultOptions: {
    queries: { retry: 2, staleTime: 30_000 },
    mutations: { retry: 0 },
  },
});
  • Queries retry twice and are considered fresh for 30 seconds.
  • Mutations never retry — money movements must not be silently re-fired.

Because the provider renders a real QueryClientProvider, your own useQuery/useMutation calls inside the tree resolve the same client and share its cache.

Bring your own QueryClient

Pass queryClient to the provider to share a client you configure yourself (your own defaults, devtools, persistence):

"use client";

import { QueryClient } from "@tanstack/react-query";
import { CapxulProvider } from "@capxul/sdk-react";

const queryClient = new QueryClient({
  defaultOptions: { queries: { retry: 1 } },
});

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <CapxulProvider publishableKey={publishableKey} queryClient={queryClient}>
      {children}
    </CapxulProvider>
  );
}

The client is pinned at mount: swapping the queryClient prop later is ignored. Create it once (module scope or useState initializer) and leave it.

Ownership also changes cleanup behavior — see what clears when.

The key namespace

Every SDK query key is an array whose first element is the literal "capxul". The catalog is hierarchical; representative shapes:

DataKey
Profile["capxul", "profile"]
Personal balance["capxul", "accountBalance"]
Org list["capxul", "orgs"]
One org["capxul", "org", orgId]
Org members / roles / treasury["capxul", "org", orgId, "members" | "roles" | "treasury"]
Payments list["capxul", "payments"]
One payment["capxul", "payments", paymentId]
Actor-scoped reads (address book and inbox)["capxul", "actor", "account" | "org", …]
Destinations["capxul", "destinations", …]

Parameterized keys use the placeholder "pending" while their parameter is still undefined (the query is disabled in that state, so nothing fetches under a placeholder key).

The catalog object itself is internal — the stable contract is the "capxul" prefix plus the exact key documented on each hook's reference page (under "Query key"). If you call the Core SDK directly, put your custom keys under the same prefix.

Automatic invalidation

You should rarely invalidate SDK data yourself. Two families of mutations do it for you:

Money mutations — personal and Organization payment or Commitment hooks invalidate, on success:

  • the payments list and the affected payment's detail key,
  • the personal balance — or, for an org actor, that org's account and treasury keys,
  • the actor's address book (for pay, which can create a new counterparty).

Auth boundary changes:

  • identity operations update the provider-owned actor directly; rich-data queries keep their own TanStack Query authorities.
  • successful useCapxulAuth().signOut() cancels and resets the authenticated rich-data query boundary.

Org mutations invalidate their own slices: useCapxulCreateOrg → the org list; useCapxulInviteMember → that org's member list. Each hook's reference page documents its exact cache behavior.

Manual invalidation

For anything the automatic rules do not cover (for example a server-driven change you learn about out of band), use useQueryClient from @tanstack/react-query. To target every Capxul query, predicate on the first key element:

"use client";

import { useQueryClient } from "@tanstack/react-query";

const queryClient = useQueryClient();

// Refetch everything the SDK has cached:
await queryClient.invalidateQueries({
  predicate: (query) => query.queryKey[0] === "capxul",
});

// Or one slice, using the documented key shape:
await queryClient.invalidateQueries({ queryKey: ["capxul", "payments"] });

invalidateQueries with a key prefix matches hierarchically — ["capxul", "payments"] invalidates the list and every ["capxul", "payments", paymentId] detail under it.

What clears on sign-out and re-bootstrap

Two different events, two different scopes:

Sign-out resets the authenticated Capxul query boundary after the identity machine signs out. If your app stores additional sensitive queries outside that boundary, remove them by predicate after the facade call succeeds:

const auth = useCapxulAuth();
const queryClient = useQueryClient();

await auth.signOut();
queryClient.removeQueries({
  predicate: (query) => query.queryKey[0] === "capxul",
});

Re-bootstrap — the provider replacing its client (a retry after a bootstrap error, a publishableKey change, unmount) — clears client-scoped data so nothing from the previous client leaks into the next one:

  • Provider-owned QueryClient: the provider calls queryClient.clear() — everything goes, including your app's own queries on that client.
  • Your own QueryClient: the provider removes only queries whose queryKey[0] === "capxul" — your app's queries survive.

That asymmetry is the main reason to bring your own QueryClient once your app caches non-Capxul data in the same tree.

On this page