Getting started
EventRail sends events to a route on your own domain and into storage you own. This page walks through the smallest working setup: an event fired in the browser, posted to a route in your app and printed to your server's console. After that, write to a real destination.
Install
Next.js
npm install @eventrail/nextjs1. Create the Hub
The Hub is the trusted core. It owns your destinations and adds to every event what only the server knows. Build one per app and share it. Start with the console destination: nothing to provision, no credentials.
Next.js · lib/eventrail-hub.ts
import "server-only";
import { createHub } from "@eventrail/nextjs/server";
import { createConsoleDestination } from "@eventrail/nextjs/destinations/console";
export const hub = createHub({
destinations: [createConsoleDestination()],
});2. Add the collector route
The Collector is the HTTP route the browser
producer posts events to. allowedOrigins lists the origins that may
post, your app's own included (see Collector options).
Next.js · app/api/eventrail/route.ts
import { createCollector } from "@eventrail/nextjs/server";
import { hub } from "@/lib/eventrail-hub";
const collector = createCollector(hub, {
allowedOrigins: ["http://localhost:3000"],Add your production origin before you deploy.
});
export const POST = collector.POST;3. Add the browser producer
Build one producer per app and share its track.
Next.js · lib/eventrail-client.ts
"use client";
import { createBrowserProducer } from "@eventrail/nextjs/client";
export const producer = createBrowserProducer({
collectorLocation: "/api/eventrail",
storeSessionId: "enabled",
producerName: "browser",
});
export const { track } = producer;storeSessionId is the storage policy: it decides when the
session identifier cookie is written. "enabled"
always stores it, which keeps this walkthrough short. The other policies are "disabled",
"optIn" and "optOut". Pick the one that matches your consent setup before you ship: see
Consent.
4. Fire your first event
Next.js · app/buy-button.tsx
"use client";
import { track } from "@/lib/eventrail-client";
export function BuyButton() {
return <button onClick={() => track("buy_clicked", { sku: "A1" })}>Buy</button>;
}Put the button on any page and click it. The full EventRecord prints in the terminal running your dev server:
{
specVersion: 1,
id: "1f2a9c3e-6b8d-4f1a-9e2c-7d4b5a6f8e10",
sessionId: "9d2c7e4b-1f8a-4c3d-8e2b-7a6f5e4d3c2b",
name: "buy_clicked",
location: "http://localhost:3000/",
occurredAt: "2026-09-23T12:00:00.081Z",
sentAt: "2026-09-23T12:00:00.092Z",
receivedAt: "2026-09-23T12:00:00.140Z",
producerType: "remote",
producerName: "browser",
producerProperties: { sku: "A1" },
hubProperties: {
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
forwardedFor: "::1",
acceptLanguage: "en-US,en;q=0.9"
},
consent: "unset"
}What you are looking at:
name,locationandproducerPropertiescame from the browser. Treat them as claims.sessionIdandconsentwere read by your server from the request's cookies, never from the event body.consentis"unset"because the visitor has not made a consent decision yet;sessionIdis there because of the"enabled"policy.producerTypeis"remote"because the event came in over HTTP through the collector.hubPropertieswas filled by the Hub from the request headers.forwardedForis the rawX-Forwarded-Forheader; in Next.js, the dev server sets it to a loopback address. Turn any of the three off, or add your own values, with the Hub options. To add the signed-in user, see Server-resolved user IDs.
That is the whole loop. Every field, and where its value comes from, is on EventRecord.
Next steps
Write to a real destination
Every destination on the Hub receives every record, independently. Keep the console one next to a real one while you build, or run several at once.
Next.js · lib/eventrail-hub.ts
import "server-only";
import { createHub } from "@eventrail/nextjs/server";
import { createFileDestination } from "@eventrail/nextjs/destinations/file";
import { createPostgresDestination } from "@eventrail/nextjs/destinations/postgres";
import { createBigQueryDestination } from "@eventrail/nextjs/destinations/bigquery";
export const hub = createHub({
destinations: [
// NDJSON on disk: local development, or a self-hosted app with a persistent filesystem.
createFileDestination({ path: ".data/events.ndjson" }),
// npm install pg
createPostgresDestination({
pool: { connectionString: process.env.DATABASE_URL, max: 5 },
}),
// npm install @google-cloud/bigquery
createBigQueryDestination({
projectId: "my-project",
datasetId: "analytics",
}),
],
});The Postgres and BigQuery destinations write to a versioned table, eventrail_events_v1 by
default, with snake_case columns such as session_id and hub_properties. Both create the table
on their first write if it doesn't exist. See Destinations
for what each one needs.
Track events from the server
For events the browser never sees, such as webhooks, completed payments and scheduled jobs, create
a server producer on the same Hub. Its track has the same shape as
the browser's.
Next.js · lib/eventrail-server.ts
import "server-only";
import { createServerProducer } from "@eventrail/nextjs/server";
import { hub } from "./eventrail-hub";
export const { track } = createServerProducer(hub, { producerName: "server" });In server code, call this track instead of the browser one; the call stays the same. If you
want a wrong import to stand out, give it another name, such as trackServer.
These events arrive with producerType: "local". The Hub still reads the current request's
cookies, so an event fired while handling a visitor's request carries their
sessionId; one fired from a webhook has none.
Record page views and uncaught errors
The browser producer comes with two ready-made trackers that call track for you. Add each once,
near the root of your app:
Next.js · app/layout.tsx
import { TrackErrors, TrackPageViews } from "@eventrail/nextjs/client";
import { track } from "@/lib/eventrail-client";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<TrackPageViews track={track} />
<TrackErrors track={track} />
</body>
</html>
);
}The page view tracker records a page_view on the first load and every time the URL changes.
The error tracker records uncaught exceptions and unhandled promise rejections as error events.
In Next.js, both are Client Components already, so your layout stays a Server Component. What each
one records, and how to record the errors they can't see, is on Trackers.
For Next.js, a working reference app that exercises every entry point, including consent and error
tracking, lives in examples/playground in the
@eventrail/nextjs repository.