# Upgrade to DatabuddyProvider

> @databuddy/sdk 3.5 adds one provider for tracking, identity and flags, typed flags and events, and one name per function. Existing code keeps working.


import { Callout, CodeBlock } from "@/components/docs";

`@databuddy/sdk` 3.5 adds `DatabuddyProvider`, so you set your client ID and signed-in user once for tracking, identity and flags. Flags are declared with their type and default, events can be type-checked, and every function has one name.

<Callout type="info">
  Nothing breaks when you update. `<Databuddy />`, `<FlagsProvider>`, `useFlag("key")`, `useFlags()`, the flat options and the older function names still work, and your editor marks them deprecated. Move over one call site at a time.
</Callout>

The Node client (`@databuddy/sdk/node`), server flags, `/agents` and `/mcp` are unchanged.

## React and Next.js

`<Databuddy />` and `<FlagsProvider>` can become one `<DatabuddyProvider>`. Pass the signed-in user, or `null` when signed out, and the provider keeps the profile and flag targeting in sync, so you no longer call `identify()` or `clearProfile()` yourself.

<CodeBlock language="tsx">
  {`// Before
<Databuddy clientId="..." trackErrors trackWebVitals />
<FlagsProvider clientId="..." user={{ userId: user.id, email: user.email }} isPending={isPending}>
  {children}
</FlagsProvider>

// After
<DatabuddyProvider
  clientId="..."
  autocapture={{ errors: true, webVitals: true }}
  user={user ? { id: user.id, traits: { email: user.email, plan } } : null}
  userPending={isPending}
>
  {children}
</DatabuddyProvider>`}
</CodeBlock>

A provider without children only loads tracking. Flags load the first time a component reads one, so apps that don't use flags make no flag requests. To render flags on the server, see [Render flags on the server](/docs/sdk/feature-flags#render-flags-on-the-server).

`disabled` on `DatabuddyProvider` also turns off flags, which then serve their defaults. If you passed `disabled` to `<Databuddy />` only to skip tracking in development, your flags will serve defaults there after you switch.

## Feature flags

Declare each flag once with its default, then read it with `useFlag`. It returns the value, typed by the default, and never `undefined`: the default is served until flags load, when the flag is missing, and on errors.

<CodeBlock language="tsx">
  {`// Before
const flag = useFlag("new-checkout");
if (flag.loading) return <Skeleton />;
return flag.on ? <NewCheckout /> : <Checkout />;

// After
import { defineFlag } from "@databuddy/sdk";
import { useFlag, useFlagDetails } from "@databuddy/sdk/react";

export const newCheckout = defineFlag("new-checkout", { default: false });
export const theme = defineFlag<"light" | "dark">("theme", { default: "light" });

const enabled = useFlag(newCheckout); // boolean
const { value, loading, status, variant } = useFlagDetails(theme);`}
</CodeBlock>

Both forms read from either provider, so you can switch hooks before or after switching providers.

| Deprecated | Use instead |
| --- | --- |
| `useFlag(key)` | `useFlag(flag)` for the value, `useFlagDetails(flag)` for status and variant |
| `useFlags().isOn(key)`, `getValue(key)` | `useFlag(flag)` |
| `useFlags().isReady`, `lastError`, `refresh()` | `useFlagsStatus()` |
| `useFlags().updateUser()` | The provider's `user` prop |
| `defaults={{ key: value }}` | `defineFlag(key, { default: value })` |
| `isPending` | `userPending` |
| `user={{ userId, email, organizationId, teamId, properties }}` | `user={{ id, organizationId, teamId, traits }}` with `email` in `traits`. Traits are also saved on the profile and must be strings, numbers, booleans or null |
| `skipStorage` | `flags={{ persist: false }}` |
| `apiUrl` (flags API), `environment` | `flags={{ apiUrl, environment }}` |
| `autoFetch` | Not needed: `DatabuddyProvider` loads flags when one is first read |
| `cacheTtl`, `staleTime`, `maxCacheSize`, `requestTimeoutMs` | No provider equivalent; keep `FlagsProvider` if you tune them |
| `disabled` (flags only) | The provider's `disabled` also turns off tracking |

A value whose type doesn't match the declared default is ignored, and the default is served with reason `TYPE_MISMATCH`.

## Functions

| Deprecated | Use instead |
| --- | --- |
| `clear()`, `clearProfile()` | `reset()`, which also starts a new anonymous ID and session, as `clear()` does |
| `setTraits(traits)` | `identify(id, traits)` |
| `getAnonymousId()`, `getSessionId()`, `getTrackingIds()`, `getProfileId()` | `getIds()` returns `{ anonymousId, sessionId, profileId }` |
| `getTracker()`, `isTrackerAvailable()` | Not needed: calls made before the tracker loads are queued |
| `createScript()`, `createReplayScript()`, `isScriptInjected()`, `stopScript()` | `init(options)`, which returns a function that stops tracking |
| `window.databuddyOptOut()`, `window.databuddyOptIn()` | `optOut()`, `optIn()` |

`trackError(error, properties)` now also accepts an `Error`. `track`, `identify`, `flush`, `setGlobalProperties`, `getTrackingParams` and `getRevenueMetadata` are unchanged; `getRevenueMetadata()` no longer needs the client ID.

## Options

These apply to `DatabuddyProvider`, `<Databuddy />`, `init()`, the Vue `<Databuddy>` component and the Nuxt module (`@databuddy/nuxt` 3.1.0 or later). When both forms are set, the grouped option wins.

| Deprecated | Use instead |
| --- | --- |
| `trackInteractions`, `trackAttributes`, `trackOutgoingLinks`, `trackErrors`, `trackWebVitals`, `trackHashChanges` | `autocapture: { interactions, attributes, outgoingLinks, errors, webVitals, hashChanges }`, or `true` / `false` for all |
| `batchSize`, `batchTimeout` | `batching: { size, timeoutMs }` |
| `enableBatching` | `batching: false` |
| `enableRetries`, `maxRetries`, `initialRetryDelay` | `retries: false` or `{ max, initialDelayMs }` |
| `samplingRate` | `sampleRate` |
| `skipPatterns`, `maskPatterns` | `skipPaths`, `maskPaths` |

## Typed events

Declare your events to type-check `track()`. Until you declare any, `track()` accepts any name and properties, as before.

<CodeBlock language="ts">
  {`import { track } from "@databuddy/sdk";

declare module "@databuddy/sdk" {
  interface Events {
    signup_completed: { plan: "free" | "pro" };
    invite_sent: { role: string } | undefined;
  }
}

track("signup_completed", { plan: "pro" });`}
</CodeBlock>

## PostHog adapter

`posthog.init()` accepts the Databuddy options in both forms, and the grouped one wins. Its `autocapture` object takes Databuddy's keys (`errors`, `webVitals`, `outgoingLinks`, ...); PostHog's own `autocapture: true` and its allowlist keys are ignored. `capture_exceptions` and `capture_performance` still turn on error and Web Vitals tracking when neither Databuddy form sets them.
