Updated 2 October 2026
Analytics for Next.js
Privacy-friendly, open-source analytics that lives inside your Next.js app: one component, one route handler, and events that go through your own domain.
In short
npm i trckable, then one component and one route in the App Router. About 2,264 bytes gzipped added to your bundle, by CI's measurement.- Events go through your own domain, so there is no separate script file for a blocklist to match by name, and Safari keeps the visitor cookie for 400 days instead of 7.
- Add Stripe, Paddle, Lemon Squeezy, Polar or Dodo, and revenue shows up per source and page.
Install#
npm i trckable
The package is MIT licensed and has no dependencies. It talks to a trckable server: your own (it is free to self-host) or trckable Cloud, where the host is https://cloud.trckable.com.
Add the component once, in the root layout:
// app/layout.tsx
import { Analytics } from 'trckable/next'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Analytics site="tkb_a1b2c3d4" />
</body>
</html>
)
}
Then add the route that carries events to your server:
// app/api/e/route.ts
export { POST } from 'trckable/next'
Set two environment variables. Your server's address, and the site's proxy key, which you find in Settings → General → For developers. The proxy key stays on your server, never in the browser.
# .env
TRCKABLE_HOST=https://stats.example.com
TRCKABLE_PROXY_KEY=your-proxy-key
That is the whole install. Route changes in the App Router are counted as page views automatically, once each.
Prefer a command? npx trckable init looks at the project and prints exactly what it would change, and writes only with --yes. npx trckable doctor reads only: it checks the server, the script, the proxy key and that visitors are seen at their own address.
Why through your own domain#
A script loaded from an analytics host is something ad blockers recognise by name. An event sent to /api/e on your own domain, from code that is part of your own bundle, looks like your app talking to itself, because that is what it is.
That is also why this is the setup we call the accurate one, not the unblockable one. Nothing is unblockable, and a determined blocker can still block anything. What changes is that there is no analytics file to match.
Three more things follow from the route being yours:
- Safari keeps returning visitors. A cookie written by JavaScript is capped at seven days in Safari. A cookie set by your own server route lasts 400 days, so a visitor who comes back after two weeks is still the same visitor.
- Do Not Track and Global Privacy Control are passed on, so a site that honours them still does.
- Visitors are seen at their own address and country, because the route forwards it with the proxy key.
The key matters. Without it the route answers with an error that says what to set, rather than quietly counting every visitor as your server. npx trckable doctor --proxy checks it.
What you get#
- Page views, goals and engagement: SPA navigations, outbound links, downloads, scroll goals and time on page.
- Events that survive a bad moment. If an event cannot be sent, it is kept in the browser and sent on a later page. The server counts each one once.
- Counted once, however many copies load. If the tag and the package both end up on a page, the second does nothing.
- A query that changes on the same page is not a new page. A search box that keeps its text in the address bar counts as one page.
- Leave out your own visits by opening your site once with
?trckable=ignore.
For goals, a call or no code at all:
import { track } from 'trckable'
track('signup', { plan: 'pro' })
<button data-trckable-goal="signup" data-trckable-goal-plan="pro">Start trial</button>
Selling something? Add revenue#
If you charge through Stripe, Paddle, Lemon Squeezy, Polar or Dodo Payments, connect the provider with one key in Settings → Payments. Each payment is then credited to the visit that earned it, so revenue shows next to traffic: per source, per page, per campaign.
When your server creates the checkout, pass the visitor along with one helper:
import { getIds, checkoutFields } from 'trckable/server'
import { cookies } from 'next/headers'
const ids = getIds(await cookies())
await stripe.checkout.sessions.create({ ...params, ...checkoutFields('stripe', ids, 'subscription') })
Plain checkout links need no code: the tracker adds the visitor when the link is clicked. See revenue attribution and the page for your provider, such as Stripe.
How big is it#
CI measures the size and fails the build when it grows. The figures below are CI's, for 1 Oct 2026.
| What | Size, gzipped |
|---|---|
trckable/react added to your app | 2,264 B (CI fails above 2,560) |
| The script tag, pageviews only | 1,596 B |
| The script tag with goals, outbound links and checkout attribution | 2,045 B |
How each number is measured is on the benchmarks page.
Questions#
- Does it work with the Pages Router or plain React?
- The component and route above are written for the App Router. For plain React apps (Vite, React Router, Remix), use
trckable/react, which takes the site id and your server's address. Anything else can callinit({ site, host })once on the client, and the script tag works anywhere. - Do I have to self-host?
- No. Point
TRCKABLE_HOSTat your own server, or athttps://cloud.trckable.comif you let us run it. The program is the same, and self-hosting is free. - Does it slow my site down?
- The tracker adds about 2 KB gzipped to your bundle (the table above has CI's figure) and sends its events in the background with
keepalive. In the proxied setup the browser makes no request to a separate analytics host. - How is this different from Vercel Analytics?
- trckable is open source, you can host it yourself, it is not tied to a hosting platform, and it credits payments to the visits behind them. For what Vercel's analytics offers, see Vercel's own documentation.
Keep reading#
- The npm package in the docs and the same-origin proxy
- Revenue attribution
- Cookieless and GDPR analytics
- Accuracy: the suite we run in three browsers