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
Session["capxul", "session"]
Profile["capxul", "profile"]
Account lifecycle["capxul", "accountLifecycle"]
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, inbox, insights)["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 drop to the raw client, 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 mutationsuseCapxulPay, useCapxulPayout, useCapxulWithdraw (and useCapxulCancelPayment when it lands) invalidate, on success:

  • the payments list and the affected payment's detail key,
  • the acting entity's insights summary and history,
  • 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:

  • useCapxulVerifyOtp (and the account-lifecycle retry) invalidates session, profile, account lifecycle, and balance — a background refetch, so mounted screens update without hard resets.
  • useCapxulSignOut resets those same keys after cancelling in-flight balance fetches — cached authenticated rows are dropped immediately, not refetched.
  • The onboarding completion hooks invalidate profile and account lifecycle (the organization variant also invalidates the org list).

Org mutations invalidate their own slices: useCapxulCreateOrg → the org list; useCapxulInviteMember / useCapxulRemoveMember / useCapxulAssignRole → 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 exactly the auth boundary: session, profile, account lifecycle, and balance. Other capxul keys (org lists, payments, address book) stay cached; their next fetch runs against the signed-out session and fails with NOT_AUTHENTICATED. If your UX needs a hard wipe of all Capxul data at sign-out, remove by predicate after the mutation succeeds:

const signOut = useCapxulSignOut();
const queryClient = useQueryClient();

await signOut.mutateAsync();
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