Amplitude implementation
Authentication tracking
User IDs flowing through every event. Sign-up and sign-in events firing reliably. The foundation for retention.
This module addresses the fundamental limitation of analytics: cookie-based tracking can't reliably identify the same user across sessions, devices, or cookie expirations. By the end of this chapter, you'll have user IDs flowing through every event, sign-up and sign-in events firing reliably, and your foundation for retention and lifecycle analysis in place.
The events
We'll track three events:
| Event | When it fires |
|---|---|
Sign-up completed | When a new user successfully registers and a user record is created |
Sign-in completed | When an existing user successfully authenticates |
Sign-out completed | When a user signs out and the session is terminated |
Each event updates user properties as it fires:
| Event | User properties set |
|---|---|
Sign-up completed | Set: email, customer_id, account_type. Set once: initial_signup_date |
Sign-in completed | Set: last_login_date, is_logged_in: true |
Sign-out completed | Set: is_logged_in: false |
setOnce differs from set: a setOnce property only takes the first value ever written. If a user signs up today, initial_signup_date is set to today and never changes, even if they sign in repeatedly.
Server-side vs. client-side
These events can be tracked from either the browser or your server. Each has trade-offs (covered in Client-side vs. server-side tracking).
Server-side is the recommendation for auth events. The reasoning: auth events drive lifecycle and retention analysis. They need to be accurate. Server-side tracking eliminates the ~10–20% loss to ad blockers and ensures the event fires precisely when authentication succeeds.
The trade-off is complexity: server-side requires passing the user's device ID and session ID from the browser so the events stitch into the user's active browsing session.
This chapter covers server-side. The client-side alternative is shorter and is included at the end for teams that prefer simplicity over accuracy.
Passing device and session IDs to the server
For server-side events to attach to the user's browsing session, your server needs the device ID and session ID generated by the browser SDK. You have two options.
Option 1: Read from the SDK and include in your request
The browser SDK exposes getDeviceId() and getSessionId(). Call them and include the values when submitting the auth form:
"use client";
import * as amplitude from "@amplitude/analytics-browser";
async function handleSignIn(formData: FormData) {
const deviceId = amplitude.getDeviceId();
const sessionId = amplitude.getSessionId();
if (deviceId) formData.set("amplitudeDeviceId", deviceId);
if (sessionId) formData.set("amplitudeSessionId", sessionId.toString());
await signInAction(formData);
}Caveat: these methods return undefined if called before init completes. In practice this isn't a problem if your forms load slightly after the SDK initializes; the SDK is normally ready by the time a user interacts with a form.
Option 2: Parse from the Amplitude cookie
If you can't easily call SDK methods (server-rendered forms, third-party integrations), the SDK stores the same values in a cookie named AMP_ + first 10 characters of your API key. The cookie value is base64-encoded, dot-separated:
const cookieName = `AMP_${API_KEY.substring(0, 10)}`;
const decoded = Buffer.from(cookieValue, 'base64').toString();
const parts = decoded.split('.');
const deviceId = parts[0];
const sessionId = parts[3] ? parseInt(parts[3], 10) : undefined;Option 1 is cleaner when it's available. Option 2 is the fallback.
Sign-up completed (server-side)
The pattern: set user properties first (via identify), then track the event. Both calls include the user ID, device ID, and session ID.
import { track, identify, Identify } from "@amplitude/analytics-node";
export async function signUpAction(formData: FormData) {
const email = formData.get("email") as string;
const deviceId = formData.get("amplitudeDeviceId") as string;
const sessionId = formData.get("amplitudeSessionId") as string;
// Create the user in your database
const user = await createUser({ email, password: formData.get("password") });
// Set user properties
const identifyObj = new Identify();
identifyObj.set("email", user.email);
identifyObj.set("account_type", "free");
identifyObj.setOnce("initial_signup_date", new Date().toISOString());
await identify(identifyObj, {
user_id: user.id,
device_id: deviceId,
}).promise;
// Track the event
await track("Sign-up completed", { method: "email" }, {
user_id: user.id,
device_id: deviceId,
session_id: sessionId ? parseInt(sessionId, 10) : undefined,
}).promise;
return { success: true, userId: user.id };
}After the server returns, set the user ID on the browser SDK so subsequent client-side events get attributed correctly:
const result = await signUpAction(formData);
if (result.success) {
amplitude.setUserId(result.userId);
router.push("/dashboard");
}Without that setUserId() call on the client, your basic-module page views and feature events would continue firing under the anonymous device ID. Server-side and client-side events would belong to the same user in Amplitude, but until the user's next session, your client-side events wouldn't carry the user ID.
Sign-in completed (server-side)
Same pattern. Identify first to update properties, then track.
export async function signInAction(formData: FormData) {
const email = formData.get("email") as string;
const deviceId = formData.get("amplitudeDeviceId") as string;
const sessionId = formData.get("amplitudeSessionId") as string;
const user = await authenticateUser(email, formData.get("password"));
if (!user) return { success: false };
const identifyObj = new Identify();
identifyObj.set("last_login_date", new Date().toISOString());
identifyObj.set("is_logged_in", true);
await identify(identifyObj, {
user_id: user.id,
device_id: deviceId,
}).promise;
await track("Sign-in completed", { method: "password" }, {
user_id: user.id,
device_id: deviceId,
session_id: sessionId ? parseInt(sessionId, 10) : undefined,
}).promise;
return { success: true, userId: user.id };
}And on the client after sign-in returns:
if (result.success) {
amplitude.setUserId(result.userId);
router.push("/dashboard");
}Sign-out completed
Sign-out has an ordering wrinkle. The server-side track call needs to happen with the user's ID attached. Then, on the client, you reset the SDK so subsequent events look like a fresh anonymous user.
// Server action
export async function signOutAction(formData: FormData) {
const userId = await getCurrentUserId();
const deviceId = formData.get("amplitudeDeviceId") as string;
const sessionId = formData.get("amplitudeSessionId") as string;
await destroySession();
const identifyObj = new Identify();
identifyObj.set("is_logged_in", false);
await identify(identifyObj, { user_id: userId }).promise;
await track("Sign-out completed", {}, {
user_id: userId,
device_id: deviceId,
session_id: sessionId ? parseInt(sessionId, 10) : undefined,
}).promise;
return { success: true };
}// Client
const result = await signOutAction(formData);
if (result.success) {
amplitude.reset(); // sever the identity
amplitude.flush(); // send any pending events
router.push("/");
}The order is: server tracks the event with the user's ID → client calls reset() to clear the user ID and rotate to a fresh device ID. If you reset first and then track, the sign-out event ends up attributed to the new anonymous user.
Initialize the SDK with the user ID
If a user is already signed in when they visit your site, you want Amplitude to know who they are before any page view events fire. Otherwise the page view ends up tagged with an anonymous device ID, and only later events (after the user does something authenticated) pick up the user ID.
Update your Amplitude init to fetch the current user and pass the ID:
// src/components/amplitude-client.tsx
"use client";
import * as amplitude from "@amplitude/analytics-browser";
import { createClient } from "@/utils/supabase/client";
async function initAmplitude() {
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
await amplitude.init(process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY!, {
fetchRemoteConfig: false,
autocapture: {
elementInteractions: false,
formInteractions: false,
pageViews: {
trackHistoryChanges: "pathOnly",
},
},
userId: user?.id,
}).promise;
}
if (typeof window !== "undefined") {
initAmplitude();
}
export const Amplitude = () => null;
export default amplitude;If the user isn't logged in, user?.id is undefined and the SDK initializes anonymously — which is correct. Once they sign in, your setUserId() call from the sign-in flow takes over.
Client-side alternative
If you'd rather track from the browser, the calls look like this:
// Sign-up
const result = await signUpAction(formData);
if (result.success) {
amplitude.setUserId(result.userId);
const identifyObj = new amplitude.Identify();
identifyObj.set("email", formData.get("email"));
identifyObj.set("account_type", "free");
identifyObj.setOnce("initial_signup_date", new Date().toISOString());
amplitude.identify(identifyObj);
await amplitude.track("Sign-up completed", { method: "email" }).promise;
await amplitude.flush().promise;
router.push("/dashboard");
}The await track().promise and flush() calls force the event to send before the redirect. Without them, the page unload can drop the event.
For sign-out:
await amplitude.track("Sign-out completed").promise;
await amplitude.flush().promise;
amplitude.reset();
router.push("/");Pattern is the same: identify first, then track, then redirect.
Validation
Before relying on auth events, walk through this checklist.
Events fire at the right moment.
- Sign-up event fires only after the server confirms the account was created — not on button click
- Sign-in event fires only on successful authentication
- Sign-out fires before the SDK is reset
User identification works.
- After sign-up/sign-in, the user ID appears on the browser SDK (
amplitude.getUserId()returns the right value) - After sign-out, the user ID is cleared and a new device ID is generated
- A returning logged-in user has their user ID set before the first page view fires
User properties are correct.
email,customer_id,account_type,initial_signup_dateare set on sign-uplast_login_dateupdates on sign-in butinitial_signup_datedoesn't change (it was setOnce'd)is_logged_inflips correctly
Cross-session continuity.
- A user who returns after their cookie expires still gets their user ID set (via the init call reading session)
- A user who signs in on a second device gets their existing user ID attached
Edge cases.
- Refreshing the page mid-auth doesn't fire duplicate events
- An already-logged-in user loading the page initializes the SDK with their ID
- An ad blocker doesn't break the client-side reset (the server-side track event still fires)