---
title: "How to stitch client and server events to one user"
description: "Share one device ID between browser and server, send both IDs at sign-up, set one canonical user ID everywhere, and reset on sign-out."
canonical: "https://amplitude.com/en-us/build/stitch-client-server-events"
---

# How to stitch client and server events to one user

Share one device ID between browser and server, send both IDs at sign-up, set one canonical user ID everywhere, and reset on sign-out.

Source: https://amplitude.com/en-us/build/stitch-client-server-events

---

# How to stitch client and server events to one user

Define the user ID once on the server, read the browser's device ID from a header or the Amplitude cookie, send both IDs on the sign-up event, hand the same user ID back to the browser, and reset cleanly on sign-out. About 20 minutes, all copy-paste.

- Node 20.6+
- Express
- Unified SDK 1.x
- Node SDK 1.x
- 20 min

Chris Tam · Last updated 8 October 2026

A visitor browses your pricing page anonymously, signs up through your API, and pays through a webhook. [Product analytics](https://amplitude.com/amplitude-analytics) only tells that story if Amplitude sees one person, not three.

In this tutorial, you'll share one device ID between browser and server, link it to a user ID at sign-up, use one user ID format everywhere, and reset on sign-out. It builds on the [Node.js product analytics page](/build/nodejs-product-analytics).

## What you'll build

An Express backend and a browser app that agree on who the user is. Before sign-up, browser page views and server events share the browser's device ID. The server's `Account Created` event carries both that device ID and the new user ID, which joins the anonymous history to the account. The API returns the user ID it used, and the browser sets exactly that string. On sign-out, `Signed Out` goes out under the old identity and the browser gets a fresh device ID. By the end you'll see the pre-sign-up page views and `Account Created` on one user profile in Amplitude.

## Prerequisites

- The Express app from [Add product analytics to a Node.js backend](/build/nodejs-product-analytics), with `lib/amplitude.js`, the `amplitudeIdentity` middleware, and `routes/auth.js`. This page replaces the middleware, `routes/auth.js`, and `server.js`.
- A frontend that initializes `@amplitude/unified` with `initAll` using the same project API key as the server, as on the [React product analytics page](/build/react-product-analytics).
- Node.js 20.6 or later and `"type": "module"` in `package.json`.

No Amplitude account yet? A free account is enough for this tutorial; nothing here needs a paid plan. [Sign up for free](https://app.amplitude.com/signup) to get your project API key.

## 1. Define the user ID in one place

Every Amplitude call on the server, and the value the browser passes to `setUserId`, comes from this one function.

lib/identity.js

```javascript
// The one place that turns an account into an Amplitude user ID.
// Never change this format once events have shipped.
export function amplitudeUserId(user) {
  // No account, no user ID. Never send placeholders like "null" or
  // "anonymous"; Amplitude would treat everyone as one person.
  if (user?.id == null) return undefined;

  // Prefix integer primary keys: Amplitude drops IDs shorter than
  // 5 characters, and "42" is two.
  return `user-${user.id}`;
}
```

**Why.** Amplitude can't merge two user IDs. If the browser sends `user-42` and a worker sends `42`, or one side sends an email address, that's two users forever, and fixing the code later only joins future activity. [User IDs](https://amplitude.com/docs/get-started/identify-users#best-practices-for-setting-user-ids) are case-sensitive and can't be changed after you set them, so base them on an immutable account ID rather than an email that can change. If you already send user IDs in some format, match it here instead of switching to the prefix.

## 2. Read the device ID from the header or the Amplitude cookie

Before sign-up, the device ID is the only thing tying browser and server events together. The Node.js product analytics page reads it from an `x-amplitude-device-id` header that your `fetch` calls set. Requests that can't set headers, such as a plain HTML form post or a server-rendered page load, still carry the browser SDK's cookie, so read that as a fallback.

The Experiment Node.js SDK ships an [`AmplitudeCookie` helper](https://amplitude.com/docs/sdks/experiment-sdks/experiment-node-js#access-amplitude-cookies) that parses the cookie. Install it with a cookie parser for Express:

Terminal

```sh
npm install @amplitude/experiment-node-server cookie-parser
```

This file replaces `middleware/amplitude-identity.js`:

middleware/amplitude-identity.js

```javascript
import { AmplitudeCookie } from '@amplitude/experiment-node-server';
import { amplitudeUserId } from '../lib/identity.js';

// Browser SDK 2 cookies are named AMP_ plus the first 10 characters of
// the project API key. Pass true for the Browser SDK 2 format.
const ampCookieName = AmplitudeCookie.cookieName(process.env.AMPLITUDE_API_KEY, true);

function deviceIdFromCookie(req) {
  const raw = req.cookies?.[ampCookieName];
  if (!raw) return undefined;
  try {
    return AmplitudeCookie.parse(raw, true).device_id || undefined;
  } catch {
    return undefined;
  }

export function amplitudeIdentity(req, res, next) {
  const ids = {};

  const userId = amplitudeUserId(req.user);
  if (userId) {
    ids.user_id = userId;
  }

  // The header wins; the cookie covers form posts and page loads.
  const deviceId = req.get('x-amplitude-device-id') || deviceIdFromCookie(req);
  if (deviceId) {
    ids.device_id = deviceId;
  }

  // Session, IP, and user agent, as on the server-side events page.
  const sessionId = Number(req.get('x-amplitude-session-id'));
  if (Number.isSafeInteger(sessionId) && sessionId > 0) {
    ids.session_id = sessionId;
  }
  if (req.ip) {
    ids.ip = req.ip;
  }
  const userAgent = req.get('user-agent');
  if (userAgent) {
    ids.user_agent = userAgent;
  }

  req.amplitudeIds = ids;
  next();
}
```

Mount `cookie-parser` before the middleware. This `server.js` replaces the one from the product analytics page:

server.js

```javascript
import express from 'express';
import cookieParser from 'cookie-parser';
import { amplitude } from './lib/amplitude.js';
import { amplitudeIdentity } from './middleware/amplitude-identity.js';
import { auth } from './routes/auth.js';
import { billing } from './routes/billing.js';

const app = express();
// One proxy hop (a load balancer) in front of the app, so req.ip is
// the client's address. Match this to your host.
app.set('trust proxy', 1);
app.use(express.json());
app.use(cookieParser());
// Mount your session or auth middleware here, before amplitudeIdentity,
// so req.user is populated.
app.use(amplitudeIdentity);
app.use('/auth', auth);
app.use('/billing', billing);

const port = Number(process.env.PORT ?? 3000);
const server = app.listen(port, () => {
  console.log(`Listening on ${port}`);
});

async function shutdown(signal) {
  console.log(`${signal} received, flushing Amplitude`);
  server.close();
  await amplitude.flush().promise;
  process.exit(0);
}

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
```

This middleware and the browser helper in step 4 also carry the session ID, IP address, and user agent from the [server-side events page](/build/send-events-server-side), so both pages end with the same files. If you followed that page, keep its `checkout` and `webhooks` route mounts in `server.js`.

**Why.** The cookie only reaches your server when the API shares a domain with the page that set it, so keep the header for API calls on another domain. The server reads the browser's device ID and never invents one: a server-made ID matches nothing in the browser and leaves those events on an orphan profile.

## 3. Send both IDs when someone signs up or signs in

This is the step that joins the history. The first event that carries both the browser's device ID and a user ID tells Amplitude that the anonymous activity on that device belongs to the account. This file replaces `routes/auth.js`:

routes/auth.js

```javascript
import { Router } from 'express';
import { amplitude } from '../lib/amplitude.js';
import { amplitudeUserId } from '../lib/identity.js';

export const auth = Router();

auth.post('/signup', async (req, res) => {
  // ... your sign-up logic creates the account and sets req.user
  const user = req.user;
  const userId = amplitudeUserId(user);

  // The middleware ran before the account existed, so req.amplitudeIds
  // has the device ID but no user ID yet. Add it here.
  const ids = { ...req.amplitudeIds, user_id: userId };

  amplitude.track('Account Created', { plan: user.plan }, ids);

  res.json({ ok: true, amplitudeUserId: userId });
});

auth.post('/login', async (req, res) => {
  // ... your authentication logic sets req.user
  const user = req.user;
  const userId = amplitudeUserId(user);
  const ids = { ...req.amplitudeIds, user_id: userId };

  const identify = new amplitude.Identify();
  identify.set('plan', user.plan);
  amplitude.identify(identify, ids);
  amplitude.track('Signed In', undefined, ids);

  res.json({ ok: true, amplitudeUserId: userId });
});

auth.get('/me', async (req, res) => {
  // Lets a page that loads already signed in pick up the same ID.
  res.json({ amplitudeUserId: amplitudeUserId(req.user) ?? null });
});

auth.post('/logout', async (req, res) => {
  // Still the signed-in user here, so this lands on their profile.
  amplitude.track('Signed Out', undefined, req.amplitudeIds);

  // ... your sign-out logic clears the session
  res.json({ ok: true });
});
```

**Why.** Amplitude treats anonymous events on a device as one person until it sees an event from that device with a user ID, then gives the anonymous events and the identified ones the [same Amplitude ID](https://amplitude.com/docs/data/sources/instrument-track-unique-users#user-id-follows-anonymous-events). Sending `device_id` with the server event means that link exists even if the browser never fires another event after sign-up. On login, the same pair joins a new device, like a phone or a second browser, to an account that already exists. Sending `user_id` alone would still credit the event to the right user, but it wouldn't pull in what the person did on that device before signing in.

The response returns `amplitudeUserId` so the browser never builds the string itself. If two codebases each format the ID, they drift.

**MCP.** Working in Cursor or Claude Code? Amplitude's MCP server lets your agent read your project's existing event taxonomy before it writes `track` calls, so a new auth route sends the `Signed In` you already chart instead of inventing `user_login` next to it. [Set up the MCP server](https://amplitude.com/docs/amplitude-ai/amplitude-mcp).

## 4. Set the same user ID in the browser

The browser sends the device ID and session ID on every API call and sets whatever user ID the server returns. `setUserId` and `reset` are exported by `@amplitude/unified`; `getDeviceId` and `getSessionId` read the [current identity state](https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2#read-and-update-identity-state).

src/api.js (browser)

```javascript
import * as amplitude from '@amplitude/unified';

export function apiFetch(path, options = {}) {
  const headers = { 'Content-Type': 'application/json', ...options.headers };
  const deviceId = amplitude.getDeviceId();
  if (deviceId) headers['x-amplitude-device-id'] = deviceId;
  const sessionId = amplitude.getSessionId();
  if (sessionId) headers['x-amplitude-session-id'] = String(sessionId);
  return fetch(path, { ...options, headers });
}
```

src/auth.js (browser)

```javascript
import * as amplitude from '@amplitude/unified';
import { apiFetch } from './api.js';

async function postJson(path, body) {
  const res = await apiFetch(path, { method: 'POST', body: JSON.stringify(body ?? {}) });
  if (!res.ok) throw new Error(`${path} failed with ${res.status}`);
  return res.json();
}

export async function signUp(form) {
  const { amplitudeUserId } = await postJson('/auth/signup', form);
  // Same string the server used. Don't rebuild it here.
  if (amplitudeUserId) amplitude.setUserId(amplitudeUserId);
}

export async function signIn(credentials) {
  const { amplitudeUserId } = await postJson('/auth/login', credentials);
  if (amplitudeUserId) amplitude.setUserId(amplitudeUserId);
}

// Call once on page load, after initAll.
export async function restoreIdentity() {
  const res = await apiFetch('/auth/me');
  if (!res.ok) return;
  const { amplitudeUserId } = await res.json();
  if (amplitudeUserId) amplitude.setUserId(amplitudeUserId);
}

export async function signOut() {
  await postJson('/auth/logout');
  // New device ID, no user ID: the next person here starts fresh.
  amplitude.reset();
}
```

On the React page, pass `amplitudeUserId` from your auth state into `IdentifyUser` and call `setUserId` with it, instead of building `` `user-${user.id}` `` in the component.

**Why.** Don't call `reset()` or set a new device ID during sign-up or sign-in. The [login guidance](https://amplitude.com/docs/data/data-assistant-user-solutions/login-identity-merge-engineering) is to keep one SDK instance and the same device ID across the anonymous and signed-in parts of a visit, then set the user ID. A reset at login hands the account a brand new device and strands the history you were trying to join.

## 5. Reset on sign-out, after the last event

`signOut` in step 4 already does this in the right order: the server tracks `Signed Out` under the user's IDs, and only after the response does the browser call `reset()`.

**Why.** When someone signs out without a reset, Amplitude assumes anonymous events on that device still come from the [last known user](https://amplitude.com/docs/data/sources/instrument-track-unique-users#multiple-user-ids-on-the-same-device). On a shared laptop, the next person's browsing lands on the previous person's profile. `reset()` clears the user ID and issues a new device ID, so what follows starts a new anonymous user. Reset only on an explicit sign-out. An anonymous visitor isn't a signed-out user, and resetting on every page load gives one visitor a new device ID each time.

## Verify it worked

Run `npm start` and your frontend. In a private browser window, visit two or three pages without signing in, then sign up a test account. Check in this order:

1. **Network tab.** The `POST /auth/signup` request carries `x-amplitude-device-id`, and its response includes `amplitudeUserId`. Filter for `api2.amplitude.com`: the next browser upload after sign-up has that `user_id` and the same `device_id` as the uploads before it.
2. **Process logs.** With `logLevel` at Debug outside production, the terminal prints the `track` call for `Account Created` with both `user_id` and `device_id` in its arguments, then the upload within about 10 seconds.
3. **Ingestion Debugger.** In Amplitude, click Data, select Sources, then open the Ingestion Debugger tab. The successful requests chart shows the uploads from both the browser and the server. It shows counts, not individual events.
4. **User lookup.** Go to Users > User Profiles and search the test account's `user-` ID. Within ten seconds to a minute, the event stream shows the `[Amplitude] Page Viewed` events from before sign-up, then `Account Created`, all on one profile.

Then sign out and visit one more page. In the Network tab, the next upload has a new `device_id` and no `user_id`. Search that new device ID in User Profiles: it's a separate anonymous user, not the test account.

To check the cookie fallback, submit any HTML form that posts to your server from the same domain, and log `req.amplitudeIds` in that route: `device_id` matches `amplitude.getDeviceId()` in the browser console.

## Troubleshooting

### Pre-sign-up page views aren't on the new user's profile

The device ID changed during sign-up, or the server event had no `device_id`. Check that nothing calls `reset()` or `setDeviceId` in the sign-up flow, and that `req.amplitudeIds.device_id` is set in the `/signup` route.

### One person shows up as two users

Two sources send different user ID strings: `user-42` and `42`, a different case, or an email on one side. Route every user ID through `amplitudeUserId` and have the browser use the value the API returns. Amplitude can't merge the two existing profiles, so the fix only applies to new activity.

### `device_id` is missing on form posts or page loads

The cookie name didn't match. The server's `AMPLITUDE_API_KEY` must be the same project key the browser uses, and `cookieName` and `parse` both need `true` for the Browser SDK 2 format. A request to an API on another domain doesn't carry the cookie at all; use the header there.

### The next person on a shared device appears as the previous user

Sign-out doesn't call `reset()`. Call it from your sign-out handler after the logout request finishes.

### Every signed-out user collapses into one profile

Something sends a placeholder such as `"null"`, `"undefined"`, or `"anonymous"` as the user ID. Amplitude rejects some of these outright and treats any other shared value as a single user. Send no `user_id` at all for anonymous traffic, which is what `amplitudeUserId` does.

## Full code reference

Every file this page adds or changes, in one block. `lib/amplitude.js` and `routes/billing.js` stay as they are on [the Node.js product analytics page](/build/nodejs-product-analytics). The two `src/` files live in your frontend. 6 files.

6 files, 162 lines

```javascript
// -- lib/identity.js --
export function amplitudeUserId(user) {
  if (user?.id == null) return undefined;
  return `user-${user.id}`;
}

// -- middleware/amplitude-identity.js --
import { AmplitudeCookie } from '@amplitude/experiment-node-server';
import { amplitudeUserId } from '../lib/identity.js';

const ampCookieName = AmplitudeCookie.cookieName(process.env.AMPLITUDE_API_KEY, true);

function deviceIdFromCookie(req) {
  const raw = req.cookies?.[ampCookieName];
  if (!raw) return undefined;
  try {
    return AmplitudeCookie.parse(raw, true).device_id || undefined;
  } catch {
    return undefined;
  }

export function amplitudeIdentity(req, res, next) {
  const ids = {};
  const userId = amplitudeUserId(req.user);
  if (userId) {
    ids.user_id = userId;
  }
  const deviceId = req.get('x-amplitude-device-id') || deviceIdFromCookie(req);
  if (deviceId) {
    ids.device_id = deviceId;
  }
  const sessionId = Number(req.get('x-amplitude-session-id'));
  if (Number.isSafeInteger(sessionId) && sessionId > 0) {
    ids.session_id = sessionId;
  }
  if (req.ip) {
    ids.ip = req.ip;
  }
  const userAgent = req.get('user-agent');
  if (userAgent) {
    ids.user_agent = userAgent;
  }
  req.amplitudeIds = ids;
  next();
}

// -- routes/auth.js --
import { Router } from 'express';
import { amplitude } from '../lib/amplitude.js';
import { amplitudeUserId } from '../lib/identity.js';

export const auth = Router();

auth.post('/signup', async (req, res) => {
  // ... your sign-up logic creates the account and sets req.user
  const user = req.user;
  const userId = amplitudeUserId(user);
  const ids = { ...req.amplitudeIds, user_id: userId };
  amplitude.track('Account Created', { plan: user.plan }, ids);
  res.json({ ok: true, amplitudeUserId: userId });
});

auth.post('/login', async (req, res) => {
  // ... your authentication logic sets req.user
  const user = req.user;
  const userId = amplitudeUserId(user);
  const ids = { ...req.amplitudeIds, user_id: userId };
  const identify = new amplitude.Identify();
  identify.set('plan', user.plan);
  amplitude.identify(identify, ids);
  amplitude.track('Signed In', undefined, ids);
  res.json({ ok: true, amplitudeUserId: userId });
});

auth.get('/me', async (req, res) => {
  res.json({ amplitudeUserId: amplitudeUserId(req.user) ?? null });
});

auth.post('/logout', async (req, res) => {
  amplitude.track('Signed Out', undefined, req.amplitudeIds);
  // ... your sign-out logic clears the session
  res.json({ ok: true });
});

// -- server.js --
import express from 'express';
import cookieParser from 'cookie-parser';
import { amplitude } from './lib/amplitude.js';
import { amplitudeIdentity } from './middleware/amplitude-identity.js';
import { auth } from './routes/auth.js';
import { billing } from './routes/billing.js';

const app = express();
// One proxy hop (a load balancer) in front of the app, so req.ip is
// the client's address. Match this to your host.
app.set('trust proxy', 1);
app.use(express.json());
app.use(cookieParser());
// Mount your session or auth middleware here, before amplitudeIdentity.
app.use(amplitudeIdentity);
app.use('/auth', auth);
app.use('/billing', billing);

const port = Number(process.env.PORT ?? 3000);
const server = app.listen(port, () => {
  console.log(`Listening on ${port}`);
});

async function shutdown(signal) {
  console.log(`${signal} received, flushing Amplitude`);
  server.close();
  await amplitude.flush().promise;
  process.exit(0);
}

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

// -- src/api.js (browser) --
import * as amplitude from '@amplitude/unified';

export function apiFetch(path, options = {}) {
  const headers = { 'Content-Type': 'application/json', ...options.headers };
  const deviceId = amplitude.getDeviceId();
  if (deviceId) headers['x-amplitude-device-id'] = deviceId;
  const sessionId = amplitude.getSessionId();
  if (sessionId) headers['x-amplitude-session-id'] = String(sessionId);
  return fetch(path, { ...options, headers });
}

// -- src/auth.js (browser) --
import * as amplitude from '@amplitude/unified';
import { apiFetch } from './api.js';

async function postJson(path, body) {
  const res = await apiFetch(path, { method: 'POST', body: JSON.stringify(body ?? {}) });
  if (!res.ok) throw new Error(`${path} failed with ${res.status}`);
  return res.json();
}

export async function signUp(form) {
  const { amplitudeUserId } = await postJson('/auth/signup', form);
  if (amplitudeUserId) amplitude.setUserId(amplitudeUserId);
}

export async function signIn(credentials) {
  const { amplitudeUserId } = await postJson('/auth/login', credentials);
  if (amplitudeUserId) amplitude.setUserId(amplitudeUserId);
}

export async function restoreIdentity() {
  const res = await apiFetch('/auth/me');
  if (!res.ok) return;
  const { amplitudeUserId } = await res.json();
  if (amplitudeUserId) amplitude.setUserId(amplitudeUserId);
}

export async function signOut() {
  await postJson('/auth/logout');
  amplitude.reset();
}
```

## Hand it to your agent

Prefer to have Cursor, Claude Code, or Codex CLI do the wiring? This prompt points the agent at this page and pins the decisions that matter.

Using Cursor or Claude Code? Paste this prompt

```text
Make this app's browser and server Amplitude events resolve to one user by
following https://amplitude.com/build/stitch-client-server-events. Put the
user ID format in one server function (lib/identity.js) and use it for every
user_id; keep any format we already send instead of changing it. In the
Express amplitudeIdentity middleware, read device_id from the
x-amplitude-device-id header, falling back to the Amplitude cookie via
AmplitudeCookie from @amplitude/experiment-node-server (Browser SDK 2 format)
and cookie-parser. On sign-up and login, send events with both the request's
device_id and the new user_id, and return amplitudeUserId in the response.
In the browser (@amplitude/unified), call setUserId with that returned value,
never a rebuilt string. Never call reset() or change the device ID during
sign-up or login; call reset() only after sign-out. Never send placeholder
user IDs for anonymous users. Don't invent event names; check the taxonomy
with the Amplitude MCP server if it's connected. Report every file you changed.
```

## What this unlocks

- [Session replay](https://amplitude.com/session-replay) recordings from before sign-up sit on the same profile as `Account Created`, so you can watch what someone did on the pricing page before they converted.
- [Feature Experiment](https://amplitude.com/amplitude-experiment) can bucket a visitor by device ID before sign-up and still credit `Account Created` to the same person.
- [Guides and surveys](https://amplitude.com/guides-and-surveys) can target signed-in users by the `plan` property the server sets at login, on whichever device they use next.
- [Product analytics](https://amplitude.com/amplitude-analytics) funnels from the first page view to `Account Created` count one user per person instead of breaking at the sign-up form.

One integration; everything else reads from the same events.

## Next steps

[Use caseHow to send events to Amplitude from your serverCarry session IDs and IP addresses onto server events, and dedupe webhook retries.](/build/send-events-server-side)[Web frameworksHow to add product analytics to a React appThe browser half of this setup, with page views and sign-in identity.](/build/react-product-analytics)[Web frameworksHow to add feature flags and experiments to a Node.js backendRead a flag for the same user and device IDs this page keeps aligned.](/build/nodejs-feature-flags)

Reference docs

[Track unique users →](https://amplitude.com/docs/data/sources/instrument-track-unique-users)[How Amplitude identifies your users →](https://amplitude.com/docs/get-started/identify-users)[Anonymous activity lost at login →](https://amplitude.com/docs/data/data-assistant-user-solutions/login-identity-merge-engineering)[Experiment Node.js SDK: access Amplitude cookies →](https://amplitude.com/docs/sdks/experiment-sdks/experiment-node-js#access-amplitude-cookies)[Browser SDK 2: identity state →](https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2#read-and-update-identity-state)[Amplitude MCP server →](https://amplitude.com/docs/amplitude-ai/amplitude-mcp)

Amplitude is the leading [all-in-one analytics platform](https://amplitude.com/) for building better products. Product and engineering teams use Amplitude to connect [product analytics](https://amplitude.com/amplitude-analytics) and [web analytics](https://amplitude.com/web-analytics) with [session replay](https://amplitude.com/session-replay), [heatmaps](https://amplitude.com/heatmaps), [experimentation](https://amplitude.com/amplitude-experiment), [feature flags](https://amplitude.com/feature-management), and [guides and surveys](https://amplitude.com/guides-and-surveys) in one unified workflow, with [AI agents](https://amplitude.com/ai-agents) working across all of it. Learn how Amplitude is building [self-improving products](https://amplitude.com/self-improving-products) with [Wave](https://amplitude.com/wave), helping teams turn product data into PRs.

For agents

[View raw .md](/build/stitch-client-server-events.md)[llms.txt](/llms.txt)

## Ship the instrumentation, then read the data

Once events are flowing, Amplitude turns them into funnels, retention curves, and session replays without any further setup.

[Create a free account](https://app.amplitude.com/signup)[Browse all Build tutorials](https://amplitude.com/build)
