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:
- Install packages
- Set environment variables
- Create a server instance
- Configure database adapters
- Create tables with CLI + ORM migrations
- Mount a catch-all handler at
/api/analytics/* - Create a client instance
- Add an analytics dashboard page + component
- Track server-side and client-side events
1) Install dependencies
Install the package:
pnpm add useanalyticsThen add your database runtime (only one path):
Drizzle
pnpm add drizzle-orm pg
pnpm add -D drizzle-kitPrisma
pnpm add @prisma/client
pnpm add -D prismaOptional CLI helpers:
pnpm add -D @useanalytics/cli2) Set environment variables
Create .env.local in your app:
NEXT_PUBLIC_USEANALYTICS_PROJECT_ID=acme-devNEXT_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 generateIf you prefer ORM-native migration flows, use:
- Drizzle:
pnpm drizzle-kit generatethenpnpm drizzle-kit migrate - Prisma:
pnpm prisma migrate dev(pnpm prisma migrate deployin 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.
- Import
createAnalyticsClientfrom your framework package. - Create the client instance.
- Optionally pass
baseURLif 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:
- The client starts with an anonymous
distinctId(stored in first-party storage). - After login, call
analyticsClient.identify(...)with your stable user id. - Use the same
distinctIdfor server-sideanalytics.track(...)calls.
Create/update person profile on login:
analyticsClient.identify("user_123", {
email: "ralf@example.com",
name: "Ralf Boltshauser",
plan: "pro",
});Better Auth example (recommended)
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
Consent and cookie banner (optional)
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-bannerThis 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 identifiersgranted: allow tracking + identifier persistencedenied: do not track, clear existing identifiersidentify(...)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
projectIdper 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
distinctIdacross client + server.
Mock dashboard preview
See a mocked interactive dashboard (overview, funnel, error tracking): /docs/mock-dashboard