# Migrate from PostHog

> Move a PostHog integration to Databuddy by changing imports, with PostHog's method names kept for events, identify, and feature flags


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

If your product already calls PostHog for events, identify, and feature flags, you can move it to Databuddy without rewriting those call sites. The `@databuddy/sdk` package ships adapters that keep PostHog's method names and argument shapes, so most of the migration is changing three import paths.

## Change the imports

<CodeBlock language="bash">
  {`bun remove posthog-js posthog-node
bun add @databuddy/sdk`}
</CodeBlock>

| PostHog import | Databuddy import |
| --- | --- |
| `posthog-js` | `@databuddy/sdk/posthog` |
| `posthog-js/react` | `@databuddy/sdk/posthog/react` |
| `posthog-node` | `@databuddy/sdk/posthog/node` |

## Browser

Pass your Databuddy client ID where PostHog expected a project token. PostHog transport options such as `api_host`, `defaults`, and `person_profiles` still compile and are ignored, because events always go to Databuddy.

<CodeBlock language="tsx">
  {`import posthog from '@databuddy/sdk/posthog';

posthog.init(process.env.NEXT_PUBLIC_DATABUDDY_CLIENT_ID!, {
  capture_exceptions: true,
  trackWebVitals: true,
});

posthog.identify(user.id, { email: user.email, plan: user.plan });
posthog.capture('checkout_completed', { total: 49 });

posthog.onFeatureFlags(() => {
  if (posthog.isFeatureEnabled('new-checkout')) {
    showNewCheckout();
  }
});`}
</CodeBlock>

The second argument also accepts every [Databuddy configuration option](/docs/sdk/configuration), so you can turn on Web Vitals, outgoing link tracking, or path masking in the same call.

Feature flags load asynchronously, so read them inside `onFeatureFlags()`, which runs once flags are ready and again after every reload. Calling `identify()` or `group()` re-evaluates flags for that user and group, and the callback runs again with the new values, so targeting rules on traits such as `plan` apply as soon as the user signs in.

## React

<CodeBlock language="tsx">
  {`import {
  PostHogProvider,
  useFeatureFlagEnabled,
} from '@databuddy/sdk/posthog/react';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <PostHogProvider apiKey={process.env.NEXT_PUBLIC_DATABUDDY_CLIENT_ID!}>
      {children}
    </PostHogProvider>
  );
}

function CheckoutButton() {
  const newCheckout = useFeatureFlagEnabled('new-checkout');
  return newCheckout ? <NewCheckout /> : <Checkout />;
}`}
</CodeBlock>

`usePostHog`, `useFeatureFlagVariantKey`, `useFeatureFlagPayload`, and `PostHogFeature` are available with the same signatures.

## Server

The server adapter needs a Databuddy API key with the `track:events` scope and the website ID the events belong to.

<CodeBlock language="tsx">
  {`import { PostHog } from '@databuddy/sdk/posthog/node';
import { waitUntil } from '@vercel/functions';

const posthog = new PostHog(process.env.DATABUDDY_API_KEY!, {
  websiteId: process.env.DATABUDDY_WEBSITE_ID!,
  waitUntil,
});

posthog.capture({ distinctId: user.id, event: 'invoice_paid' });

const variant = await posthog.getFeatureFlag('pricing-page', user.id, {
  personProperties: { plan: user.plan },
});`}
</CodeBlock>

In serverless functions, either pass `waitUntil` so each capture is delivered before the function freezes, or call `await posthog.shutdown()` before returning. `flushAt`, `flushInterval`, `requestTimeout`, and `disabled` keep their PostHog meaning. `getAllFlags` and `getAllFlagsAndPayloads` return every flag for a user when you bootstrap flags during server rendering.

## What changes

Most calls behave the same, but a few PostHog concepts map differently or have no Databuddy equivalent yet.

- **Pageviews are automatic.** The tracker records pageviews and page exits on every route change, so `capture('$pageview')` and `capture('$pageleave')` are ignored to avoid counting them twice.
- **Traits are flat values.** Databuddy profiles store strings, numbers, booleans, and null. Nested objects and arrays in `$set` or `identify()` are dropped, and `$set_once` is ignored because profiles have no set-once semantics.
- **Groups become properties.** `group('company', 'databuddy')` adds `group_company: "databuddy"` to later events and to flag targeting. Group analytics and `groupIdentify` are not available.
- **Some calls differ or are unavailable.** `alias` logs a warning and returns. The server adapter's `groupIdentify` also warns and returns; the browser adapter has no `groupIdentify`, so remove those browser calls. In the browser, `startSessionRecording()` warns and returns, but `stopSessionRecording()` stops Databuddy replay. Turn recordings on through Databuddy's `replay` option instead. Databuddy does not run in-app surveys.
- **Privacy options do not compile.** Options that change what is collected, such as `before_send`, `property_denylist`, `mask_all_text`, `persistence`, and `opt_out_capturing_by_default`, are rejected by the types so a migration cannot quietly collect more than your PostHog setup did. Set up the matching Databuddy controls before you remove them. [`skipPatterns` and `maskPatterns`](/docs/sdk/configuration) skip or mask URL paths, `replay: { maskText }` controls page text in recordings (masked by default), and `disabled` keeps tracking off until a visitor consents. Databuddy has no equivalent of `property_denylist` or `persistence`, so leave sensitive properties out of the events you send.

<Callout type="info">
  To keep your history, open **Import history** in your website settings and upload a PostHog batch export of your events as JSON lines. Databuddy rebuilds pageviews and sessions from the `$pageview` events.
</Callout>
