EventRail
Docs menu

Server-resolved user IDs

Attach the signed-in user to every event from your server, where a browser cannot forge it, instead of passing a user ID through track().

Whatever the browser passes to track() is claimed: anyone can call it from the browser console, or post to your collector directly, with any user ID they like. The Hub runs in your server, where it can read the visitor's session itself. What it adds goes into the record's hubProperties, apart from what the browser sent.

Add the user in hubProperties

The Hub's hubProperties function runs once for every event, from the browser and from your server alike. Look up the signed-in user there. In Next.js, it reads the request with cookies() and headers() from next/headers.

lib/eventrail-hub.ts

Next.js · lib/eventrail-hub.ts

import "server-only";
import { cookies } from "next/headers";
import { createHub } from "@eventrail/nextjs/server";
import { createConsoleDestination } from "@eventrail/nextjs/destinations/console";
import { getUserIdFromSession } from "@/lib/auth"; // your existing auth

export const hub = createHub({
  destinations: [createConsoleDestination()],
  hubProperties: async () => {
    const session = (await cookies()).get("session")?.value;
    return { userId: session ? await getUserIdFromSession(session) : null };
  },
});
  • Read the request before the first await. In Next.js, a server event can be handed over just as its request finishes, after which Next.js no longer serves request data. Above, cookies() is called before anything is awaited.
  • A failure drops the event. If hubProperties throws, for example because your session store is unreachable, the event is dropped and the error goes to the Hub's onError. The code that called track() is not affected.
  • It records the user, it doesn't turn anyone away. Events from signed-out visitors are still accepted, with userId set to null. To accept only signed-in users, see Authenticated users only.

Say a visitor signed in as usr_42 runs track("page_view", { userId: "usr_7" }) in the browser console. The record keeps both values, each in its own place (other fields left out):

{
  producerProperties: { userId: "usr_7" },Claimed by the browser: anyone can send any value.
  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: "203.0.113.7",
    acceptLanguage: "en-US,en;q=0.9",
    userId: "usr_42"Resolved by your server from the session cookie.
  }
}

Full records are on EventRecord.

With an auth library

In Next.js, with one of the common auth libraries, hubProperties calls the library's own session check. Each example is a complete lib/eventrail-hub.ts.

Auth.js

import "server-only";
import { createHub } from "@eventrail/nextjs/server";
import { createConsoleDestination } from "@eventrail/nextjs/destinations/console";
import { auth } from "@/auth"; // your Auth.js config

export const hub = createHub({
  destinations: [createConsoleDestination()],
  hubProperties: async () => {
    const session = await auth();
    return { userId: session?.user?.id ?? null };Auth.js leaves the id out of the session until your session callback adds it.
  },
});

Events without a signed-in user

Not every event has a signed-in user behind it. With the hubProperties above:

EventuserId
A click from a visitor signed in as usr_42"usr_42"
A server action that visitor triggers"usr_42": it runs in their request
A click from a signed-out visitornull
A webhook, or a cron job that calls one of your routesnull: the request has no session cookie
A queue worker or a scriptNo record: the event is dropped

Signed-out visitors keep their sessionId when they sign in, as long as the storage policy stores one. Events from before sign-in can then be related to the user through it:

namesessionIduserId
page_view9d2c7e4b-1f8a-4c3d-8e2b-7a6f5e4d3c2bnull
plan_upgraded9d2c7e4b-1f8a-4c3d-8e2b-7a6f5e4d3c2b"usr_42"

Outside a request, such as in a queue worker or a script, there is no request to read. In Next.js, cookies() throws there, so hubProperties throws and every event is dropped. If you fire events there, catch it:

lib/eventrail-hub.ts

Next.js · lib/eventrail-hub.ts

import "server-only";
import { cookies } from "next/headers";
import { createHub } from "@eventrail/nextjs/server";
import { createConsoleDestination } from "@eventrail/nextjs/destinations/console";
import { getUserIdFromSession } from "@/lib/auth"; // your existing auth

export const hub = createHub({
  destinations: [createConsoleDestination()],
  hubProperties: async () => {
    let cookieStore;
    try {
      cookieStore = cookies();
    } catch {
      return { userId: null };No request, so nobody is signed in.
    }
    const session = (await cookieStore).get("session")?.value;
    return { userId: session ? await getUserIdFromSession(session) : null };
  },
});

With an auth library, its session check throws outside a request as well. Keep the try around cookies(), and call the library after it. Catching the library's own errors instead would also hide real failures, such as an unreachable session store, as signed-out events.