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.tgzNEXT_PUBLIC_X3_KEY={{PUBLISHABLE_KEY}}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:
// 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
npm run dev, open the site in a normal browser, scroll and click a link.- The network tab shows
POST {{API_ORIGIN}}/v1/ewith status 204. - 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).
X3_API_ORIGIN={{API_ORIGIN}}
{{PROXY_KEY_ENV}}=sk_3x_YOUR_PROXY_KEYimport { 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}},
});<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).
Consent
Without a banner nothing is needed (cookieless mode). With your own banner, report the decision
through the x3() helper from lib/x3.ts:
"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
"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.