Skip to main content
WebNext.jsAmplitude

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-browser

Setup in 5 steps

  1. 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. 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. 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. 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. 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

Before

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
}
After

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

Yes. If you are still on the Pages Router, the client wrapper works anywhere in pages/ and components/. For server code, use the Node wrapper inside getServerSideProps or API routes. The split is less rigid than App Router but the same two wrappers apply.
Inside a client component rendered at the top of your tree, typically mounted from app/layout.tsx. Initializing in a Server Component fails because window is undefined on the server.
Use a Server Action or Route Handler. Server Components are for rendering, they cannot hold side effects like fire-and-forget analytics calls reliably during a streaming render.
Functionally yes, pricing-wise very different. Amplitude Data is tightly integrated with Amplitude and priced as part of their Enterprise plan. Ordaze has a self-serve free tier and works with any analytics vendor, not just Amplitude.
The Amplitude Node SDK requires Node runtime. For Edge routes, send events via the Amplitude HTTP V2 API directly, Ordaze's HTTP codegen target emits a typed fetch wrapper suitable for the Edge.

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