docs

Frameworks

Buoy needs two things in any React web app:

  1. The early hook, import '@buoy-gg/core/web/register', before React DOM loads. It lets Highlight Updates and the other render tools see React, and it starts recording fetch and XHR calls so Network shows the requests the page made before Buoy loaded.
  2. The host, FloatingDevTools, inside your providers, loaded only in the browser and only for the people who should see it.

The examples load the host in development only. To show Buoy to admins or QA in a production build, see Production builds.

Where each one goes depends on the framework. Each setup below comes from a test app that Buoy's framework test suite installs the way you would, then checks tool by tool. The examples mount a DevTools component like the one in Installation.

Vite + React

Put the hook on the first line of src/main.tsx:

tsx
import '@buoy-gg/core/web/register';
import { createRoot } from 'react-dom/client';
import { App } from './App';

createRoot(document.getElementById('root')!).render(<App />);
import '@buoy-gg/core/web/register';
import { createRoot } from 'react-dom/client';
import { App } from './App';

createRoot(document.getElementById('root')!).render(<App />);

Load the host lazily behind import.meta.env.DEV, so it stays out of the production bundle:

tsx
import { lazy, Suspense } from 'react';

const DevTools = import.meta.env.DEV ? lazy(() => import('./DevTools')) : () => null;

export function App() {
  return (
    <Providers>
      {/* your app */}
      <Suspense fallback={null}>
        <DevTools licenseKey={import.meta.env.VITE_BUOY_KEY} />
      </Suspense>
    </Providers>
  );
}
import { lazy, Suspense } from 'react';

const DevTools = import.meta.env.DEV ? lazy(() => import('./DevTools')) : () => null;

export function App() {
  return (
    <Providers>
      {/* your app */}
      <Suspense fallback={null}>
        <DevTools licenseKey={import.meta.env.VITE_BUOY_KEY} />
      </Suspense>
    </Providers>
  );
}

Next.js App Router

Next runs instrumentation-client.ts in the project root before the app hydrates. Put the hook there:

ts
// instrumentation-client.ts
import '@buoy-gg/core/web/register';
// instrumentation-client.ts
import '@buoy-gg/core/web/register';

Load the host from a client component with next/dynamic and ssr: false, and render it in the root layout inside your providers:

tsx
// app/BuoyDevTools.tsx
'use client';

import dynamic from 'next/dynamic';

const DevTools =
  process.env.NODE_ENV === 'development'
    ? dynamic(() => import('./DevTools'), { ssr: false })
    : () => null;

export function BuoyDevTools() {
  return <DevTools licenseKey={process.env.NEXT_PUBLIC_BUOY_KEY} />;
}
// app/BuoyDevTools.tsx
'use client';

import dynamic from 'next/dynamic';

const DevTools =
  process.env.NODE_ENV === 'development'
    ? dynamic(() => import('./DevTools'), { ssr: false })
    : () => null;

export function BuoyDevTools() {
  return <DevTools licenseKey={process.env.NEXT_PUBLIC_BUOY_KEY} />;
}

This works with Turbopack and with webpack (next dev --webpack).

Next.js Pages Router

Put the hook in instrumentation-client.ts, as for the App Router, and mount the host from pages/_app.tsx with the same next/dynamic pattern.

The Pages Router loads React DOM before instrumentation-client.ts runs. Network and the other tools work without any extra step. Highlight Updates also needs a small inline script in pages/_document.tsx, which runs before any of Next's bundles:

tsx
// pages/_document.tsx
import { Head, Html, Main, NextScript } from 'next/document';
import { earlyHookScript } from '@buoy-gg/core/web/register';

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <script dangerouslySetInnerHTML={{ __html: earlyHookScript }} />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}
// pages/_document.tsx
import { Head, Html, Main, NextScript } from 'next/document';
import { earlyHookScript } from '@buoy-gg/core/web/register';

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <script dangerouslySetInnerHTML={{ __html: earlyHookScript }} />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

The script keeps a reference to React DOM, and the hook in instrumentation-client.ts passes it to Buoy. When the hook doesn't run, the script only leaves an unused placeholder, so it can stay in production builds.

React Router framework mode

Put the hook on the first line of app/root.tsx. entry.client.tsx is too late, because React Router loads the route modules, and React DOM with them, before it.

tsx
// app/root.tsx
import '@buoy-gg/core/web/register';
import { lazy, Suspense, useEffect, useState } from 'react';

const DevTools = import.meta.env.DEV ? lazy(() => import('./DevTools')) : () => null;

// Buoy is browser-only, so it renders after hydration.
function ClientDevTools() {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  if (!mounted) return null;
  return (
    <Suspense fallback={null}>
      <DevTools licenseKey={import.meta.env.VITE_BUOY_KEY} />
    </Suspense>
  );
}
// app/root.tsx
import '@buoy-gg/core/web/register';
import { lazy, Suspense, useEffect, useState } from 'react';

const DevTools = import.meta.env.DEV ? lazy(() => import('./DevTools')) : () => null;

// Buoy is browser-only, so it renders after hydration.
function ClientDevTools() {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  if (!mounted) return null;
  return (
    <Suspense fallback={null}>
      <DevTools licenseKey={import.meta.env.VITE_BUOY_KEY} />
    </Suspense>
  );
}

Render <ClientDevTools /> in the root route's component, inside your providers.

TanStack Start

Start's default client entry is generated for you. Add src/client.tsx with the same code plus the hook on the first line:

tsx
// src/client.tsx
import '@buoy-gg/core/web/register';
import { StartClient } from '@tanstack/react-start/client';
import { StrictMode } from 'react';
import { hydrateRoot } from 'react-dom/client';

hydrateRoot(
  document,
  <StrictMode>
    <StartClient />
  </StrictMode>,
);
// src/client.tsx
import '@buoy-gg/core/web/register';
import { StartClient } from '@tanstack/react-start/client';
import { StrictMode } from 'react';
import { hydrateRoot } from 'react-dom/client';

hydrateRoot(
  document,
  <StrictMode>
    <StartClient />
  </StrictMode>,
);

Mount the host from the root route's component with the same ClientDevTools wrapper as React Router, inside your providers.

Production builds

Buoy also works in production builds, for example for admins, developers or QA testers using the live site. Two things change:

  • The key. Outside development, FloatingDevTools renders only with a Pro key.

  • Who sees it. Load the host for the users who should have it, instead of behind a development check. Buoy doesn't know your roles, so the check is yours:

    tsx
    const DevTools = lazy(() => import('./DevTools'));
    
    function MaybeDevTools() {
      const user = useCurrentUser();
      if (!user?.isAdmin) return null;
      return (
        <Suspense fallback={null}>
          <DevTools licenseKey={import.meta.env.VITE_BUOY_KEY} />
        </Suspense>
      );
    }
    
    const DevTools = lazy(() => import('./DevTools'));
    
    function MaybeDevTools() {
      const user = useCurrentUser();
      if (!user?.isAdmin) return null;
      return (
        <Suspense fallback={null}>
          <DevTools licenseKey={import.meta.env.VITE_BUOY_KEY} />
        </Suspense>
      );
    }
    

Keep the early hook import where it is. In a production build it does nothing for most visitors: it runs only in a browser where FloatingDevTools has rendered in the past week. So the first page an admin opens after signing in lists requests from when Buoy loaded, and later page loads include the requests made while the page loads. Highlight Updates works the same way. When someone signs out on a shared browser, call clearReleaseOptIn() from @buoy-gg/core/web to stop it there.

The Desktop connection has its own production switch: pass externalSync={{ enableInRelease: true }} to connect from a production build. It connects to Buoy Desktop on the admin's own machine, and Desktop asks once whether to allow your site. See Desktop and MCP for the other production rules.

Account key variable

Pass the key through a variable your framework exposes to the browser: VITE_BUOY_KEY in Vite, React Router and TanStack Start, and NEXT_PUBLIC_BUOY_KEY in Next.js. buoy login picks the right one from your package.json.

Other frameworks

Buoy's web build is React, so it runs in any React app whose bundler reads the browser export condition. In a framework not listed here, put the hook wherever code runs before React DOM loads. If nothing runs that early, put it as early as you can: Network then shows requests from that point on, and Highlight Updates won't see renders. The other tools don't depend on the hook.