Type-safe Amplitude in Next.js
Amplitude ships a browser SDK and a Node SDK. Next.js App Router runs code in both places. Ordaze generates a typed wrapper for each runtime from a single tracking plan, and the scanner verifies both client and server call sites in CI.
Analytics in Next.js App Router has two environments to worry about. Client components run in the browser and use @amplitude/analytics-browser. Server Components, Route Handlers, and Server Actions run on Node and use @amplitude/analytics-node. They initialize differently, they authenticate differently (client uses a public key, server uses a private key), and they send different default properties.
That split breaks most typed-analytics setups, which assume one SDK. Ordaze generates two wrappers from the same tracking plan, one for the browser, one for Node, and the scanner understands that a call inside a "use client" file is different from the same call inside a Server Action.
The Amplitude SDK
This guide assumes you are installing the official Amplitude SDK. The typed wrapper Ordaze generates sits on top of it and calls into it directly. You are not replacing your analytics provider, just adding a type-safe layer.
npm install @amplitude/analytics-browserSetup in 5 steps
- 1
Install both Amplitude SDKs
The browser SDK is for client components. The Node SDK is for Server Actions, Route Handlers, and background jobs. Both are needed if your tracking plan fires events from both sides.
npm install @amplitude/analytics-browser @amplitude/analytics-node - 2
Initialize Amplitude in a client provider
Create a client component that initializes amplitude-browser on mount and wraps your app. Keep the public key in NEXT_PUBLIC_AMPLITUDE_KEY so it is safe to ship to the browser.
// app/analytics-provider.tsx "use client"; import { useEffect } from 'react'; import * as amplitude from '@amplitude/analytics-browser'; export function AnalyticsProvider({ children }: { children: React.ReactNode }) { useEffect(() => { amplitude.init(process.env.NEXT_PUBLIC_AMPLITUDE_KEY!, { defaultTracking: false, }); }, []); return <>{children}</>; } - 3
Define events in Ordaze and generate both wrappers
In the Ordaze dashboard, export the Amplitude codegen twice: once for the browser target (TypeScript + analytics-browser) and once for the Node target (TypeScript + analytics-node). Both files expose the same typed method names; they differ only in which SDK they call.
- 4
Use the client wrapper in components, the Node wrapper in Server Actions
Import from analytics/events.client.ts inside "use client" files. Import from analytics/events.server.ts inside Server Actions and Route Handlers. The method signatures are identical, so moving a call between client and server only changes the import.
- 5
Run the scanner in CI on both client and server code
The Ordaze scanner reads all TypeScript source, identifies which files are client components vs server code via Next.js conventions, and fails the PR if either side drifts from the plan.
Before and after
Two SDKs, two untyped call sites, one way to typo the event
// components/UpgradeButton.tsx
"use client";
import * as amplitude from '@amplitude/analytics-browser';
export function UpgradeButton() {
return <button onClick={() => {
amplitude.track('Upgrade Started', { Plan: 'pro' });
}}>Upgrade</button>;
}
// app/api/checkout/route.ts
import { track } from '@amplitude/analytics-node';
export async function POST() {
track('upgrade_completed', { plan: 'pro' }, { user_id: userId });
// ^ different casing vs 'Upgrade Completed' used on the client
}Same typed API in both places. Types enforce the plan.
// components/UpgradeButton.tsx
"use client";
import { track } from '@/analytics/events.client';
export function UpgradeButton() {
return <button onClick={() => {
track.upgradeStarted({ plan: 'pro' });
}}>Upgrade</button>;
}
// app/api/checkout/route.ts
import { track } from '@/analytics/events.server';
export async function POST() {
track.upgradeCompleted({ plan: 'pro' }, { userId });
}Next.js + Amplitude gotchas
Browser SDK needs a public key, Node SDK needs a private key
NEXT_PUBLIC_AMPLITUDE_KEY is visible to any user. The Node SDK's API key is not, and must stay in a server-only environment variable. Mixing them up either leaks a private key or causes every browser event to be rejected.
Default tracking double-counts page views in SPAs
Amplitude's defaultTracking includes page views. Next.js already fires a route change on client navigation, which the SDK turns into another event. Either disable defaultTracking or disable your explicit page view event, never both on.
Server Actions do not automatically carry user context
When you call track from a Server Action, Amplitude has no device ID or session. You must pass the user ID explicitly. Wrap the Node track with a helper that reads the session and forwards user_id on every call.
Amplitude Ampli is a parallel codegen, pick one
Ampli is Amplitude's own typed wrapper generator. It requires ampli.json and a separate CLI. Running both Ampli and Ordaze against the same plan will produce two sets of typed files with different shapes. Use one.
Why Ordaze for this combo
One plan, two runtimes
Client code and server code share the same conceptual event list. Ordaze generates one typed file per runtime from a single tracking plan, so adding an event updates both sides in one commit.
Scanner understands Next.js conventions
The scanner recognizes "use client" directives, Route Handlers, and Server Actions. It catches a client-only call placed in a server file, and vice versa.
No Ampli lock-in
Amplitude Ampli is tied to Amplitude. If you decide to move to Mixpanel or add PostHog later, your typed wrapper has to be rewritten. Ordaze codegen is vendor-agnostic, swap the generated file, keep the call sites.
Frequently asked questions
Keep reading
Amplitude, typed, client and server
Free plan includes 100 events, full TypeScript codegen for both runtimes, and the scanner. No credit card.
Get Started Free