Initialize userorbit-js from one persistent Client Component. Mount that component from the App Router root layout or the Pages Router custom App, then identify the signed-in person after authentication is ready. Pass only the public Account ID and the minimum user and account fields needed for targeting across the client boundary.

Before you begin

You need:

  • a Next.js application using App Router, Pages Router, or both
  • the public Account ID from Userorbit → Settings → Widget
  • a stable application user ID and, for B2B applications, a stable account or workspace ID
  • a staging route and test account
  • a published tour or checklist if you want to verify delivery

The Account ID is safe to expose in browser code. Do not expose API keys, access tokens, signing secrets, or server-only session data.

1. Install the package

Use the package manager already used by your application:

npm install userorbit-js
Bash
pnpm add userorbit-js
Bash

Add the public Account ID to your client environment:

NEXT_PUBLIC_USERORBIT_ACCOUNT_ID=YOUR_PUBLIC_ACCOUNT_ID
Bash

Next.js includes NEXT_PUBLIC_ variables in the client bundle. That is appropriate for the Userorbit Account ID, but never for a secret.

2. Create one persistent Client Component

Create components/userorbit-client.tsx:

"use client"

import { useEffect } from "react"
import userorbit from "userorbit-js"

const accountId = process.env.NEXT_PUBLIC_USERORBIT_ACCOUNT_ID

let sdkReady: Promise<void> | undefined

export function ensureUserorbit() {
  if (!accountId) {
    return Promise.reject(
      new Error("Missing NEXT_PUBLIC_USERORBIT_ACCOUNT_ID"),
    )
  }

  sdkReady ??= userorbit.init({
    accountId,
    apiHost: "https://api.userorbit.com",
    floatingWidget: true,
  })

  return sdkReady
}

export type UserorbitIdentity = {
  id: string
  email?: string
  name?: string
  role?: string
  accountId: string
  accountName?: string
  accountPlan?: string
}

export function UserorbitClient({ user }: { user?: UserorbitIdentity }) {
  useEffect(() => {
    void ensureUserorbit()
  }, [])

  useEffect(() => {
    if (!user?.id) return

    void ensureUserorbit().then(() =>
      userorbit.identify(user.id, {
        email: user.email,
        name: user.name,
        attributes: {
          "user.role": user.role,
          "account.id": user.accountId,
          "account.name": user.accountName,
          "account.plan": user.accountPlan,
        },
      }),
    )
  }, [
    user?.id,
    user?.email,
    user?.name,
    user?.role,
    user?.accountId,
    user?.accountName,
    user?.accountPlan,
  ])

  return null
}

export async function logoutUserorbit() {
  await userorbit.logout()
  sdkReady = undefined
}
TSX

The module-level promise deduplicates initialization during React Strict Mode's extra development cycle. Userorbit remains entirely behind a browser boundary.

The current web SDK identifies a person. It does not expose a separate company or group identification method. Use stable custom attributes such as account.id, account.name, account.plan, and user.role to describe the active B2B tenant.

Call logoutUserorbit() from your application's real sign-out path. If a person switches tenants without signing out, pass the updated account fields so identify() runs again.

3. Mount it with App Router

Mount the Client Component from a layout that persists across the routes where Userorbit should run. A Server Component can resolve authentication, but only pass the minimum serializable fields needed by the browser component.

import { UserorbitClient } from "@/components/userorbit-client"
import { getCurrentUser } from "@/lib/auth"

export default async function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  const session = await getCurrentUser()

  const user = session
    ? {
        id: session.user.id,
        email: session.user.email,
        name: session.user.name,
        role: session.membership.role,
        accountId: session.account.id,
        accountName: session.account.name,
        accountPlan: session.account.plan,
      }
    : undefined

  return (
    <html lang="en">
      <body>
        <UserorbitClient user={user} />
        {children}
      </body>
    </html>
  )
}
TSX

The authentication names above are examples. Map them to your own session model. Do not serialize access tokens or server-only authorization state into the component.

4. Mount it with Pages Router

Render the same component from pages/_app.tsx so it survives client-side navigation:

import type { AppProps } from "next/app"
import { UserorbitClient } from "@/components/userorbit-client"
import { useSession } from "@/lib/use-session"

export default function App({ Component, pageProps }: AppProps) {
  const session = useSession()

  const user = session
    ? {
        id: session.user.id,
        email: session.user.email,
        name: session.user.name,
        role: session.membership.role,
        accountId: session.account.id,
        accountName: session.account.name,
        accountPlan: session.account.plan,
      }
    : undefined

  return (
    <>
      <UserorbitClient user={user} />
      <Component {...pageProps} />
    </>
  )
}
TSX

Do not call init() from getServerSideProps, a Server Component, or another server-only module.

5. Verify route changes before adding a fallback

The current SDK observes common History API, popstate, and hash changes. App Router and Pages Router client navigation should therefore be tested before adding manual route notifications.

Use Analytics → Live Events and navigate between several real application routes. Each real navigation should produce one $uo_web_page_view with the expected URL.

If a real route is missing, add a narrow fallback.

For App Router:

"use client"

import { useEffect } from "react"
import { usePathname, useSearchParams } from "next/navigation"
import userorbit from "userorbit-js"

export function UserorbitRouteFallback() {
  const pathname = usePathname()
  const searchParams = useSearchParams().toString()

  useEffect(() => {
    void userorbit.registerRouteChange()
  }, [pathname, searchParams])

  return null
}
TSX

For Pages Router:

import { useEffect } from "react"
import { useRouter } from "next/router"
import userorbit from "userorbit-js"

export function UserorbitPagesRouteFallback() {
  const router = useRouter()

  useEffect(() => {
    const handleRoute = () => {
      void userorbit.registerRouteChange()
    }

    router.events.on("routeChangeComplete", handleRoute)
    return () => router.events.off("routeChangeComplete", handleRoute)
  }, [router.events])

  return null
}
TSX

Add only the fallback for the router you actually use. If one navigation then produces two page-view events, remove it because automatic detection already covers your application.

6. Configure targeting and launch an experience

Create and publish the tour or checklist in Userorbit first. Restrict the initial test to a staging URL and a known account attribute such as account.id = acct_test.

To start a published checklist from client code:

"use client"

import userorbit from "userorbit-js"

export function OpenSetupChecklist() {
  async function openChecklist() {
    await ensureUserorbit()
    await userorbit.startChecklist("YOUR_PUBLISHED_CHECKLIST_ID")
  }

  return <button onClick={openChecklist}>Open setup checklist</button>
}
TSX

Use userorbit.startTour("YOUR_PUBLISHED_TOUR_ID") for a published tour.

These methods do not create or publish an experience. If the experience does not render, confirm its ID, published state, URL rule, audience, frequency, schedule, and target element.

7. Track a configured product event

Create the code event in Userorbit before calling it from the application:

await userorbit.track("workspace_created", {
  hiddenFields: {
    workspaceId: workspace.id,
    template: workspace.templateKey,
  },
})
TypeScript

Fire the event only after the underlying product action succeeds. Keep payloads small and do not send credentials or arbitrary customer data.

8. Verify App Router and Pages Router separately

If both routers exist during a migration, test them as separate delivery surfaces.

Check App Router Pages Router
Initialization Root layout mounts one Client Component _app.tsx mounts one Client Component
Server boundary Only serializable user/account fields cross it Auth hook resolves in the browser
Route evidence App Router URLs appear once in Live Events Pages Router URLs appear once in Live Events
Identity Contact has the expected account.* values Contact has the expected account.* values
Delivery Published experience renders on a target route Published experience renders on a target route
Logout Userorbit session clears with application auth Userorbit session clears with application auth

Also verify the shared browser layers:

  • userorbit.umd.cjs loads without a CSP or network error.
  • Settings → Widget reports recent SDK activity.
  • One eligible and one ineligible staging user prove that targeting is correct.
  • Tour or checklist completion and dismissal are recorded.
  • A configured product event responds exactly once.

Troubleshooting

window is not defined or the SDK runs during rendering

  • Confirm the integration component begins with "use client" for App Router.
  • Keep SDK imports and calls out of server-only files.
  • Do not call init() from getServerSideProps or a Server Component.

The SDK initializes twice in development

  • Keep the module-level sdkReady promise.
  • Do not mount the client component from multiple layouts.
  • Verify production behavior separately from React Strict Mode's development checks.
  • Check Live Events before adding a fallback.
  • Add registerRouteChange() only for a demonstrated missed navigation.
  • Remove the fallback if automatic detection and the manual hook both fire.

The wrong account receives an experience

  • Pass the active account.id, not a stale account from an earlier session.
  • Re-identify after tenant switching.
  • Call logout() when application auth signs out.
  • Match attribute keys and values exactly in the targeting rule.

The experience does not render

  • Confirm it is published in the same workspace as the Account ID.
  • Check URL rules against the actual client URL.
  • Confirm the staging contact matches the audience.
  • Wait until the target element exists.
  • Check prior completion, dismissal, frequency, and schedule state.

Implementation ownership

Userorbit provides the browser SDK, person identity, custom attributes, configured events, targeting, tours, checklists, Live Events, and delivery analytics. Next.js provides the server/client and routing boundaries. Your application owns authentication, serializable identity mapping, stable IDs, tenant switching, consent, CSP, event correctness, and regression testing.

For framework-neutral details, see How to Install the Userorbit SDK in a React Application. For additional failure modes, see SDK Troubleshooting Reference in the help center.

Was this helpful?