EventRail
Docs menu

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/nextjs

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

lib/eventrail-hub.ts

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

app/api/eventrail/route.ts

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.

lib/eventrail-client.ts

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

app/buy-button.tsx

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, location and producerProperties came from the browser. Treat them as claims.
  • sessionId and consent were read by your server from the request's cookies, never from the event body. consent is "unset" because the visitor has not made a consent decision yet; sessionId is there because of the "enabled" policy.
  • producerType is "remote" because the event came in over HTTP through the collector.
  • hubProperties was filled by the Hub from the request headers. forwardedFor is the raw X-Forwarded-For header; 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.

lib/eventrail-hub.ts

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.

lib/eventrail-server.ts

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:

app/layout.tsx

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.