Skip to Content
DocsV1 Getting started

V1 Getting Started

This guide is the north-star DX for useanalytics v1 in a Next.js App Router app.

It covers the full setup path:

  1. Install packages
  2. Set environment variables
  3. Create a server instance
  4. Configure database adapters
  5. Create tables with CLI + ORM migrations
  6. Mount a catch-all handler at /api/analytics/*
  7. Create a client instance
  8. Add an analytics dashboard page + component
  9. Track server-side and client-side events

1) Install dependencies

Install the package:

pnpm add useanalytics

Then add your database runtime (only one path):

Drizzle

pnpm add drizzle-orm pg pnpm add -D drizzle-kit

Prisma

pnpm add @prisma/client pnpm add -D prisma

Optional CLI helpers:

pnpm add -D @useanalytics/cli

2) Set environment variables

Create .env.local in your app:

NEXT_PUBLIC_USEANALYTICS_PROJECT_ID=acme-dev

NEXT_PUBLIC_USEANALYTICS_PROJECT_ID is required and must match tracker + ingest.

3) Create the analytics instance

Create lib/analytics.ts:

import { useAnalytics } from "useanalytics"; export const analytics = useAnalytics({ // uses NEXT_PUBLIC_USEANALYTICS_PROJECT_ID by default });

4) Configure database with adapters

Now wire your database into the same lib/analytics.ts file.

Drizzle adapter

import { useAnalytics } from "useanalytics"; import { drizzleAdapter } from "useanalytics/adapters/drizzle"; import { db } from "@/lib/db"; export const analytics = useAnalytics({ database: drizzleAdapter(db), });

Prisma adapter

import { useAnalytics } from "useanalytics"; import { prismaAdapter } from "useanalytics/adapters/prisma"; import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); export const analytics = useAnalytics({ database: prismaAdapter(prisma), });

5) Create tables with the CLI

useanalytics includes a CLI to help manage the schema required by the library.

  • Generate: Generates ORM schema artifacts or migration files.
pnpm dlx @useanalytics/cli@latest generate

If you prefer ORM-native migration flows, use:

  • Drizzle: pnpm drizzle-kit generate then pnpm drizzle-kit migrate
  • Prisma: pnpm prisma migrate dev (pnpm prisma migrate deploy in production)

See the CLI documentation for more information.

6) Mount handler (/api/analytics/*)

Create app/api/analytics/[...all]/route.ts:

import { analytics } from "@/lib/analytics"; import { toNextJsHandler } from "useanalytics/next-js"; export const { POST, GET } = toNextJsHandler(analytics);

7) Create client instance

The client-side library lets you track events from the browser.

  1. Import createAnalyticsClient from your framework package.
  2. Create the client instance.
  3. Optionally pass baseURL if your analytics server is on a different domain.

If your handler uses a custom base path, pass the full ingest path (for example http://localhost:3000/custom-path/analytics/ingest).

Create lib/analytics-client.ts:

import { createAnalyticsClient } from "@useanalytics/next"; export const analyticsClient = createAnalyticsClient({ /** * Optional if analytics API is on the same domain. * baseURL: "http://localhost:3000", */ baseURL: "http://localhost:3000", ingestPath: "/api/analytics/ingest", // optional when using default });

Identity and person profiles

For privacy-first setups, you can stay anonymous (no identify call).

If you want person profiles, use this model:

  1. The client starts with an anonymous distinctId (stored in first-party storage).
  2. After login, call analyticsClient.identify(...) with your stable user id.
  3. Use the same distinctId for server-side analytics.track(...) calls.

Create/update person profile on login:

analyticsClient.identify("user_123", { email: "ralf@example.com", name: "Ralf Boltshauser", plan: "pro", });

Create a tiny client bridge component:

"use client"; import { useEffect } from "react"; import { useSession } from "better-auth/react"; import { analyticsClient } from "@/lib/analytics-client"; export function AnalyticsIdentity() { const { data: session } = useSession(); useEffect(() => { const user = session?.user; if (!user) return; analyticsClient.identify(user.id, { email: user.email ?? undefined, name: user.name ?? undefined, }); }, [session?.user]); return null; }

Mount it once in your layout:

import { AnalyticsIdentity } from "@/components/analytics-identity"; export default function RootLayout(props: { children: React.ReactNode }) { return ( <html lang="en"> <body> <AnalyticsIdentity /> {props.children} </body> </html> ); }

Reset identity on logout:

analyticsClient.reset();

Recommended storage defaults:

  • distinctId: first-party cookie + localStorage (when identify/persistence is enabled)
  • sessionId: sessionStorage (rotates per browser session, optional)
  • person properties: stored server-side via identify updates

If you enable identity persistence (identify, distinctId, sessionId), add consent gating.

Initialize the client with a pending default:

import { createAnalyticsClient } from "@useanalytics/next"; export const analyticsClient = createAnalyticsClient({ baseURL: "http://localhost:3000", ingestPath: "/api/analytics/ingest", consent: "pending", // pending | granted | denied });

Install the drop-in banner component:

pnpx useanalytics add cookie-banner

This generates a local component (shadcn-style source ownership), for example: components/analytics-cookie-banner.tsx.

Mount it once in your root layout:

import { AnalyticsCookieBanner } from "@/components/analytics-cookie-banner"; import { analyticsClient } from "@/lib/analytics-client"; export default function RootLayout(props: { children: React.ReactNode }) { return ( <html lang="en"> <body> {props.children} <AnalyticsCookieBanner analytics={analyticsClient} /> </body> </html> ); }

AnalyticsCookieBanner renders only while consent is pending. identify(...) is ignored while consent is pending or denied.

analyticsClient.identify("user_123", { email: "ralf@example.com", name: "Ralf Boltshauser", plan: "pro", });

Consent behavior (north-star DX):

  • pending: do not send events, do not persist identifiers
  • granted: allow tracking + identifier persistence
  • denied: do not track, clear existing identifiers
  • identify(...) before consent: no-op (silently ignored)

8) Set up dashboard page

Create app/analytics/page.tsx:

import { analytics } from "@/lib/analytics"; import { AnalyticsDashboardPage } from "useanalytics/next"; export const dynamic = "force-dynamic"; export default function AnalyticsPage() { // here you should implement your custom isAdmin Auth Check return <AnalyticsDashboardPage analytics={analytics} />; }

Basic usage

Track server-side events

From a Server Action or any server code:

"use server"; import { analytics } from "@/lib/analytics"; import { auth } from "@/lib/auth"; export async function createProjectAction() { // ... your business logic const session = await auth.api.getSession(); if (!session?.user?.id) { return; } await analytics.track({ distinctId: session.user.id, event: "project created", pathname: "/projects/new", properties: { source: "server_action" }, }); }

Track client-side events

From a client component:

"use client"; import { usePathname } from "next/navigation"; import { analyticsClient } from "@/lib/analytics-client"; export function UpgradeButton() { const pathname = usePathname(); return ( <button onClick={() => { void analyticsClient.track("upgrade clicked", { pathname, source: "pricing_page", }); }} > Upgrade </button> ); }

Notes

  • Keep one stable projectId per environment (acme-dev, acme-prod).
  • Always validate payloads on ingest (client payload is untrusted).
  • Prefer tracking via the shared server/client instances to keep event shape consistent.
  • Keep identity consistent: the same user should map to one stable distinctId across client + server.

Mock dashboard preview

See a mocked interactive dashboard (overview, funnel, error tracking): /docs/mock-dashboard

Last updated on