Analytics
Next.js boilerplate with DataFast
Web analytics that tells you which channel made money, not just which one made visits.
Revenue-first web analytics. The DataFast script loads from your own origin after hydration. A route handler forwards events, so ad blockers have no hostname to match. Goals are typed against a catalogue and stripped of personal data. Server-side goals go through the Goals API after the response. With Stripe, Polar or Dodo selected, every checkout carries the visitor id in its metadata. Revenue lands on the channel that earned it.
What DataFast adds to the agent layer: 3 rules · 1 skill · 6 solution docs · 1 MCP server
Maintained by @raviMITNext.js on Vercel
From the manifest
Should you pick DataFast?
Pick it if
Indie hackers and small SaaS teams who want one answer: which referrer, campaign or page produced revenue. Checkout attribution is wired for Stripe, Polar and Dodo. One script tag, no event plan needed on day one.
Watch out for
- It is web analytics, not product analytics. Page views, referrers, goals and revenue. No session replay, no feature flags, no cohorts over custom person properties. Pick PostHog if you need those.
- Attribution depends on one cookie. The checkout must carry the
datafast_visitor_idfrom the same browser that visited, which is what thecheckout-attributionslot does. Payments made on another device, or before consent, land as direct traffic.
Show 3 moreShow fewer
- Goal parameters are capped at 10 per event and 255 characters per value, all stored as strings. Typed properties are converted for you; numbers you want to sum belong in your database.
- Every goal counts toward your monthly event quota. Track decisions, not every click.
- The script writes a first-party cookie for a year. In the EU and the UK that needs consent.
src/lib/analytics/consent.tsships a gate you must switch on before you serve traffic there.
What it costs
No free plan. A 14-day trial with no card. Starter is $9/month (1 site, 1 team member) and Growth $19/month (30 sites, 30 team members), both at 10k monthly events. Go over and tracking continues, but you must upgrade to open the dashboard. API, CLI and MCP access are on both plans.
Prices change. Check with DataFast before you commit.
registry/tested.yaml
Tested with DataFast
Each pair was installed, typechecked, linted, built and booted together.
- Admin panel
- Admin panel
- Error tracking
- Sentry
- Customer support
- Crisp
What it adds
What DataFast adds to the repo
Read straight from the datafast manifest, so it is exactly what lands in your repo.
Environment variables
NEXT_PUBLIC_DATAFAST_WEBSITE_IDRequiredPublic, reaches the browser
The website id from DataFast. Public by design: it sits in every page's HTML as
data-website-id. Without it the script is never rendered and the build still succeeds.- Where to get it
- DataFast -> Website settings -> General (starts with dfid_)
- Placeholder
- dfid_exampledonotuse00000
NEXT_PUBLIC_DATAFAST_DOMAINOptionalPublic, reaches the browser
The root domain you registered in DataFast, used for the cookie scope. Defaults to the host of NEXT_PUBLIC_APP_URL without "www.". Set it when your app runs on a subdomain such as app.example.com.
- Where to get it
- DataFast -> Website settings -> General (the domain you added)
- Placeholder
- example.com
NEXT_PUBLIC_DATAFAST_ALLOW_LOCALHOSTOptionalPublic, reaches the browser
"true" sends events from localhost. Off by default, because the script ignores localhost on purpose and you do not want your dev clicks in the production numbers. Turn it on for an afternoon of debugging, then off.
- Placeholder
- false
DATAFAST_API_KEYOptional
Website API key, server only. Needed for server-side goals (
trackGoalServer) and for thedatafast:api-keyverify check. Not needed for page views, browser goals or revenue attribution.- Where to get it
- DataFast -> Website settings -> API -> Create API key
- Placeholder
- df_exampledonotuse0000000000000000
Dependencies
- server-only^0.0.1
MCP server
datafast
- URL
- https://datafa.st/api/mcp
Files it writes
10 files, at these exact paths.
src/8 files
app/2 files
api/1 file
events/1 file
- route.ts
js/1 file
script.js/1 file
- route.ts
lib/6 files
analytics/6 files
- attribution.ts
- consent.ts
- datafast-client.ts
- datafast-server.ts
- goals.ts
- provider.tsx
variants/2 files
with-clerk/1 file
slots/1 file
- auth-public-routes.ts
with-payments/1 file
slots/1 file
- checkout-attribution.ts
Stack slots it fills
The stack declares these injection points; this battery supplies the fragment, so the provider tree, the env check and the schema stay one file each instead of many.
- @slot env-required
- @slot legal-processors
- @slot providers
- @slot verify-checks
The differentiator
What DataFast teaches your agent
Other starter kits stop at the package. This is the part an agent reads: where it may work, what it must never do there, and the problems someone already solved.
Rules (3)
Loaded when the agent opens a matching file.
Goals come from the catalogue, are named object_verb, and never carry personal data
Loads onsrc/lib/analytics/**src/app/**src/components/**.claude/rules/goals.md
One place a goal is born
src/lib/analytics/goals.ts declares every goal this repo sends. A goal not in
GoalCatalogue does not compile. That is the point.
- Never call
window.datafast()directly. UsetrackGoal()from@/lib/analytics/datafast-clientin the browser andtrackGoalServer()from@/lib/analytics/datafast-serveron the server. - Never add
data-fast-goalattributes. They skip the catalogue, the type check and the PII filter. - A new goal means three edits in
goals.ts: the name, its property type, and its line inGOAL_QUESTIONS./add-goaldoes all three. - Page views are not goals. The script records them, App Router navigations included. Never fire a page view by hand.
Naming
object_verb, past tense, snake_case, at most 64 characters. Checked by
GOAL_NAME_PATTERN.
| Write this | Not this | Why |
|---|---|---|
checkout_started | startCheckout | A goal records something that happened |
signup_completed | signup-complete | One casing, past tense |
project_created | create_project | Object first, so the list groups by object |
onboarding_step_completed | onboarding_2_done | The step is a property, not part of the name |
- Never use a name in
RESERVED_GOAL_NAMES:payment,subscription_started,trial_started,identifyand the rest. DataFast sends those itself once a payment provider is connected. A custompaymentgoal pollutes revenue. - Never encode a value in a name.
plan_pro_boughtandplan_team_boughtare one goal with aplanproperty.
Properties
- Low-cardinality labels you will filter by: plan, source, step, method.
- At most 10 per goal, values at most 255 characters, all sent as strings.
toGoalMetadata()enforces this and drops the rest. - No personal data. DataFast's own docs pass
emailandnameas goal parameters. Do not copy that.FORBIDDEN_PROPERTY_KEYSstrips them; do not renameemailtocontactto get past it. - No free text a user typed, no ids from another company's system, no secrets.
identifyVisitor()takes your database user id. Never the email.
Which side fires it
- Browser (
trackGoal): intent and interface.checkout_started,signup_started, a button a user clicked. - Server (
trackGoalServer): facts after a write.signup_completed,onboarding_step_completed,newsletter_subscribed. NeedsDATAFAST_API_KEY. - Never both for the same name. That doubles every count.
- Money is neither. Payments reach DataFast from the payment provider. See the revenue attribution rule.
Every checkout carries the DataFast visitor id, and revenue is never a goal
Loads onsrc/lib/analytics/**src/lib/billing/**.claude/rules/revenue-attribution.md
DataFast attributes a payment to a channel by reading datafast_visitor_id
from the checkout's metadata. No metadata, no attribution: the sale shows up as
direct traffic and your best channel looks like your worst.
- Every checkout, subscription or one-off, is created with its params passed
through
withCheckoutAttribution()from@/lib/analytics/attribution. The payment batteries already do this through thecheckout-attributionslot. A new checkout path you add by hand must do it too. - Read the ids on the server, from the request cookies, inside the server action or route handler that creates the checkout. Never accept a visitor id from a request body: it is a claim, and it would let anyone attribute revenue to any channel.
- Never add revenue as a custom goal. No
paymentgoal, nopurchase_completedwith an amount. DataFast reads payments from the connected provider and de-duplicates them. A goal copy double counts. withCheckoutAttribution()only adds keys. It never replacesuserIdor any other metadata the webhook relies on.- Attribution respects consent. In
"required"mode it adds nothing until the visitor agreed. Do not bypass that by reading the cookie yourself. - The payment provider must also be connected in the DataFast dashboard. That is a one-time manual step, not code. If revenue shows as zero, check that first.
The DataFast script never blocks rendering, never skips consent, and never leaves your origin
Loads onsrc/lib/analytics/**src/app/layout.tsxsrc/app/js/**src/app/api/events/**.claude/rules/script-loading.md
Loading
- The script is rendered once, by
AnalyticsProviderinsrc/lib/analytics/provider.tsx, withnext/scriptandstrategy="afterInteractive". NeverbeforeInteractive, never a raw<script>in<head>, never a second copy in a page. - The provider renders the script beside the app, not around it. Nothing in the
tree may wait for analytics. No
await, no loading state, no Suspense boundary that depends onwindow.datafast. - A missing
NEXT_PUBLIC_DATAFAST_WEBSITE_IDmeans "render nothing". It must never throw, andnext buildmust pass with no env vars set. - Calls before the script loads are queued by
installQueue(). Do not add a second queue snippet.
Proxy
srcis/js/script.jsanddata-api-urlis/api/events. Both are route handlers in this repo that forward to datafa.st. The browser never talks to datafa.st directly. Do not "simplify"srcback tohttps://datafa.st/js/script.js: blocklists match that hostname.src/app/api/events/route.tsforwards the visitor's IP, user agent, origin and referer. Remove those and every visitor is geolocated to your server.- Both routes answer inert on upstream failure: an empty script, a
204. A failing analytics call must never put a red error in a visitor's console. - If a route path collides with one of yours, rename the folder and update the
matching constant in
datafast-client.ts. Nothing else references it.
Consent
src/lib/analytics/consent.tsowns the policy.CONSENT_MODEis the only switch.- In
"required"mode the script is not rendered, goals are not queued and checkouts carry no visitor id untilgrantAnalyticsConsent()runs. Every new code path that sends data to DataFast checkshasAnalyticsConsent(). - Proxying changes the hostname, not the law. It is never a way around consent.
Skills (1)
Invoked by name.
- /add-goal
Add a DataFast goal to the typed catalogue and fire it from the right side of the network.
.claude/skills/add-goal/SKILL.md
Solution docs (6)
Written before you hit the problem. Each one ships in the repo at docs/solutions/ and is published here as a cookbook page.
- Cookie consent for DataFast in Next.js without breaking attributionDataFast sets a year-long first-party cookie, which needs opt-in consent in the EU and UK. Gate the script render, the goal queue and the checkout metadata on one flag.docs/solutions/datafast/cookie-consent-for-datafast.md
- DataFast shows nothing on localhost? That is on purpose. How to debug it anywayThe script skips localhost, 127.0.0.1, .local hosts, iframes and browsers flagged with datafast_ignore. Turn on data-allow-localhost for a session, check the network tab, then turn it off.docs/solutions/datafast/datafast-not-tracking-localhost.md
- DataFast goals versus page views, and when you need neitherPage views are free and automatic. Goals cost events and need a name, a reason and a side of the network. A decision table, the naming rule, and what never to put in a goal.docs/solutions/datafast/goals-vs-pageviews.md
- Proxy DataFast through your Next.js domain so ad blockers stop eating visitsBlockers match the datafa.st hostname. Serve the script and the events endpoint from your own origin with two route handlers, and forward the visitor's IP.docs/solutions/datafast/proxy-datafast-through-nextjs.md
- DataFast shows revenue as direct traffic? Pass the visitor id into checkout metadataDataFast attributes a payment by reading datafast_visitor_id from the checkout's metadata. Read the cookie on the server when you create the Stripe, Polar, Lemon Squeezy or Dodo checkout.docs/solutions/datafast/revenue-attribution-checkout-metadata.md
How it fits
What DataFast needs, and what it goes well with
The resolver enforces this before it generates anything, and names every addition it makes.
Requires
Nothing. DataFast stands on its own.
Pairs well with
- A payments battery. Suggested, never added for you.
Cannot be combined with
Compared with the alternatives
Build a repo with DataFast
Free and MIT. The builder opens with DataFast picked. You download the zip right away, and we email you the link too.