Docs

Next.js App Router

Placeholders such as {{PUBLISHABLE_KEY}} are filled in when you open this recipe from the dashboard's install page or through Claude (recipe format).

Install

npm install {{API_ORIGIN}}/v1/sdk/3x-engine-sdk-0.1.0.tgz
.env.local
NEXT_PUBLIC_X3_KEY={{PUBLISHABLE_KEY}}
app/layout.tsx
import { X3Analytics } from "@3x-engine/sdk/next";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <X3Analytics publishableKey={process.env.NEXT_PUBLIC_X3_KEY!} host="{{API_ORIGIN}}" />
      </body>
    </html>
  );
}

Keep your existing layout; add the import and the <X3Analytics> element at the end of <body>. Set NEXT_PUBLIC_X3_KEY in your hosting provider's environment as well.

Add a small helper so components can call the tracker that <X3Analytics> loads – it queues calls until the script is ready and is used by the consent and event examples below:

lib/x3.ts
// Calls the tracker that <X3Analytics> loads (window.x3). Commands issued before the
// script has loaded are queued and replayed in order.
type X3Command = (...args: unknown[]) => void;

export function x3(...args: unknown[]): void {
  if (typeof window === "undefined") return;
  const w = window as unknown as { x3?: X3Command; x3q?: unknown[][] };
  w.x3 ??= (...queued: unknown[]) => void (w.x3q ??= []).push(queued);
  w.x3(...args);
}

Verify

  1. npm run dev, open the site in a normal browser, scroll and click a link.
  2. The network tab shows POST {{API_ORIGIN}}/v1/e with status 204.
  3. The dashboard's install page (or the MCP tool events_tail) shows the pageview within seconds.

A 403 means the origin is not allowed: add http://localhost:3000 or enable Allow localhost in the project's tracking settings.

Proxy (optional)

Route tracking through your own domain: ad blockers no longer interfere and the visitor's IP is forwarded with a proxy key (why). Create a proxy key in Project → Settings → Keys → Proxy keys (scope ingest:proxy).

.env.local
X3_API_ORIGIN={{API_ORIGIN}}
{{PROXY_KEY_ENV}}=sk_3x_YOUR_PROXY_KEY
app{{PROXY_PATH}}/[...path]/route.ts
import { createX3ProxyHandler } from "@3x-engine/sdk/next/proxy";

export const { GET, POST, OPTIONS } = createX3ProxyHandler({
  apiOrigin: process.env.X3_API_ORIGIN!,
  proxyKey: process.env.{{PROXY_KEY_ENV}},
});
app/layout.tsx
<X3Analytics publishableKey={process.env.NEXT_PUBLIC_X3_KEY!} host="{{PROXY_PATH}}" />

If you have middleware.ts or proxy.ts, exclude the proxy path from its matcher (do not use a folder starting with _ – those are private in the App Router).

Without a banner nothing is needed (cookieless mode). With your own banner, report the decision through the x3() helper from lib/x3.ts:

components/ConsentBanner.tsx
"use client";
import { x3 } from "@/lib/x3";

export function ConsentButtons() {
  return (
    <>
      <button onClick={() => x3("consent", "granted")}>Accept</button>
      <button onClick={() => x3("consent", "denied")}>Decline</button>
    </>
  );
}

Using a CMP (Cookiebot, Usercentrics, Complianz, Borlabs, Google Consent Mode)? Select it in the project's consent settings instead – no code (Consent).

Events

components/SignupButton.tsx
"use client";
import { x3 } from "@/lib/x3";

export function SignupButton() {
  return (
    <button
      onClick={() => {
        x3("track", "signup_click", { plan: "pro" });
        x3("goal", "trial_started");
      }}
    >
      Start trial
    </button>
  );
}

No-code alternative: <a data-x3-event="signup_click" data-x3-prop-plan="pro"> (Data attributes). Leads and conversions from server actions: Server helper.