In brief
Three pieces. A script tag in app/layout.tsx through next/script, on the default strategy. A server action that awaits cookies() from next/headers, decodes the rail_referral cookie and takes its clickId. And metadata.rail_referral on the Stripe call that creates the customer, the subscription or the checkout session. A click id is the short code that names one visit from one affiliate link, and the whole job is moving it from the cookie to that field. Consent banners are one more attribute on the tag.
No card. Cancel in one click.
How do you add affiliate tracking to a Next.js SaaS?
In shortThree pieces. One script tag in your root layout. One cookie read in a server action. One metadata field on the call that creates your Stripe customer. The script stores the affiliate's click id in a cookie. Your server reads it and hands it to Stripe. Stripe's webhook brings it back to the affiliate tool.
The research method was simple. I read the Next.js docs for scripts and cookies on 6 September 2026. Then AffiliateRail's attribution guide, its server-side attribution pages, and its Paddle guide. Everything below comes from those pages. The three-line server function is on the docs page, and this article says what each line does rather than pasting it.
Four words first. The App Router is the newer way Next.js lays out pages, with a layout file at the root. A server action is a function that runs on your server when the browser calls it, such as when a form is submitted. A first-party cookie is a cookie set on your own domain, which browsers treat as yours. And a click id is the short code that names one visit from one affiliate link. The whole job is moving a click id from a cookie to a Stripe field.
cookies is an async function that allows you to read the HTTP incoming request cookies in Server Components, and read/write outgoing request cookies in Server Functions or Route Handlers.
Where does the script go?
In shortIn the root layout, through the next/script component. The Next.js docs say that to load a third-party script for all routes, you "import next/script and include the script directly in your root layout". That is app/layout.tsx. The default loading strategy is the right one.
The strategy is a property on the Script component. The docs list four. beforeInteractive loads "before any Next.js code and before any page hydration occurs". afterInteractive is the default and loads "early but after some hydration on the page occurs". lazyOnload loads "during browser idle time". And a fourth, worker, which the docs say does not yet work with the App Router. For a tracking script the default is fine. It runs early enough to catch the referral on the first page, and it does not block your app.
What the script does once it runs is on AffiliateRail's attribution guide. A referral parameter is the part of the link that names the affiliate, such as ?ref=jimi. When a visitor lands with one in the URL, "the script records a click and stores the referral in a first-party cookie named rail_referral on your registrable domain, plus localStorage as a fallback". Registrable domain means acme.com, so www.acme.com and app.acme.com all see the same cookie. The attribution window is 60 days by default.
The script reads nineteen link parameters, in a fixed order, so your own parameter always wins. Most of the rest are the names other affiliate platforms use: via for Rewardful, fpr for FirstPromoter, tolt for Tolt, tap_s for Tapfiliate, affonso for Affonso. They are captured so links published before you moved keep converting. If the query string carries no referral, it reads the URL fragment, so a link ending #rail=jimi works too. And if your marketing site and your app sit on different registrable domains, you list the second one in the tag's data-domains attribute. The script carries the referral across in the link.
How do you read the cookie on the server?
In shortWith cookies() from next/headers, inside a server action or a route handler. In the App Router that function is asynchronous, so you await it. Then you URL-decode the cookie's value, parse the JSON, and take the clickId field. The value is null when the visitor arrived without a partner link.
The Next.js docs are precise about the first part. "cookies is an async function that allows you to read the HTTP incoming request cookies in Server Components, and read/write outgoing request cookies in Server Functions or Route Handlers." And: "You must use async/await or React's use function to access cookies." The docs say a synchronous call still works in Next.js 15 but is deprecated. So an older snippet that skips the await is on borrowed time.
AffiliateRail's Next.js page shows the rest in one small helper. It reads the rail_referral cookie. It decodes the raw value with decodeURIComponent, because the cookie holds encoded JSON. It parses that JSON. And it returns the clickId field, or null if there is none. The server-side attribution page's own line: "the field you need is clickId".
The null case matters. Its attribution page says the value is null when the visitor arrived without a partner link, and that "you should create the customer without the metadata in that case rather than sending an empty string". So the helper returns null, and the Stripe call below only attaches the field when there is something to attach.
Which Stripe call gets the metadata?
In shortWhichever one your app makes first for that person. The field is metadata.rail_referral, set to the click id. You can put it on the customer, the subscription, or the checkout session. AffiliateRail reads four Stripe events and binds the customer on the first one that carries a click id.
The guide's table lists them. A customer, read on customer.created. A subscription, read on customer.subscription.created. A checkout session, read on checkout.session.completed, from metadata or from client_reference_id. And Stripe's hosted routes, Payment Links, Pricing Tables and the Buy Button, where you set nothing, because "the script stamps client_reference_id for you".
Two rules keep this simple. "Binding is idempotent on the Stripe customer id. The first event that carries a click id wins". So if you set the field on the customer and on the subscription, nothing doubles. And a click id the program never issued "binds nothing", with the reason shown on the attribution health panel. So a stale or mistyped id cannot credit the wrong partner.
Let's say your sign-up form calls a server action that creates a Stripe customer. That action awaits cookies(), calls the helper, and passes metadata with rail_referral set to the click id when there is one. Stripe stores it on the customer. When customer.created arrives at AffiliateRail, the customer is bound to the partner. Every later invoice from that customer is attributed to that partner. What it earns is whatever your commission flow says.
What if you bill through Paddle instead?
In shortThe same idea, with a different field. On Paddle the click id goes into custom_data.rail_referral on the transaction. If your checkout opens through Paddle.js, the script fills it. If your server creates transactions, you set the same key from the cookie, with the same helper.
Paddle then does one thing Stripe does not. It copies custom_data onto the subscription it creates, and back onto every transaction that subscription creates. So the click id rides every renewal on its own. The Paddle integration page has the connection steps, and the Stripe integration page has the Stripe ones.
What about consent banners?
In shortOne attribute on the same script tag. Add data-consent="auto" and the script waits for an affirmative consent signal before it tracks. It finds the signal itself from Google Consent Mode v2, the IAB framework, Cookiebot or OneTrust. Or your banner calls window.rail.consent() when the visitor agrees.
The attribution guide says what happens before that signal. "Before consent the script makes no network request, writes no cookie, reads and writes no localStorage". The referral is held in a JavaScript variable and nowhere else. On consent, the held referral is sent and the cookie is written. Every click row records whether it was captured under consent mode, so the state is auditable per click.
For a Next.js app this is the whole change. The Script tag in the layout gains one attribute. Your consent banner, if it is a custom one, calls one function. The server action does not change, because it only ever reads a cookie that consent mode either wrote or did not.
How do you check it works?
In shortRun it in Stripe test mode. Visit your site through a partner link. Create an account, so the server action runs. Pay an invoice. Then open Insights, Attribution health, in AffiliateRail. The sale should be there with the path shown as "Metadata".
The guide says test mode "works the same way as live". If the sale lands under "Unattributed" instead, "the row says what was missing". Read the row before you change anything.
That is the full walk for a Next.js SaaS. One tag in the layout. One awaited cookies() call. One metadata field. The Stripe tracking article covers the same handshake from Stripe's side, and the cookieless tracking article covers what happens when a browser blocks the cookie.
One tag, one cookie read, one metadata field
The script fills the field on Stripe's hosted routes. Your server action fills it everywhere else, in three lines from the docs. Fourteen days, no card.
Every fact on this page was read off the Next.js docs or AffiliateRail's docs on the date beside it in the sources. Framework versions change. Read the docs page before you copy anything.
Common questions
Which next/script strategy should a tracking script use?
The default, afterInteractive. The Next.js docs say it loads early but after some hydration on the page occurs. That is soon enough to catch the referral parameter on the first page, and it does not block your app. beforeInteractive is for scripts that must run before any Next.js code. lazyOnload waits for browser idle time. The docs say the worker strategy does not yet work with the App Router.
Why does cookies() need await in the App Router?
The Next.js docs define it as an async function. You must use async/await or React's use function to access it. The docs add that a synchronous call still works in Next.js 15 but is deprecated. AffiliateRail's Next.js page shows the awaited form in a small helper that returns the clickId or null.
Which Stripe object should carry the click id?
Whichever your app creates first for that person. AffiliateRail reads it from customer.created, customer.subscription.created and checkout.session.completed, from metadata.rail_referral or from client_reference_id on a session. Binding is idempotent on the Stripe customer id, and the first event with a click id wins, so setting it in more than one place changes nothing.
Does this work with Paddle?
It does, with a different field. On Paddle the click id goes into custom_data.rail_referral on the transaction. The script fills it when the checkout opens through Paddle.js, and a server that creates transactions sets the same key from the cookie. Paddle then copies custom_data to the subscription and to every renewal, so the click id rides each renewal on its own.
Where these facts come from
Fact-checked and reviewed by Jimi Barkway on 6 September 2026. Every figure above was read off the document named here on the date beside it. To contact AffiliateRail about one, email support@affiliaterail.com and the figure is corrected and the date moved.
- The Next.js docs, Scripts guide: next/script in the root layout, and the beforeInteractive, afterInteractive, lazyOnload and worker strategieschecked 6 September 2026
- AffiliateRail docs, Attribution and tracking: the rail_referral cookie on the registrable domain, the 60-day window, nineteen parameters, the fragment, data-domains, and consent modechecked 6 September 2026
- AffiliateRail docs, Server-side attribution: the clickId field, metadata.rail_referral, the four Stripe events, idempotent binding, the null case, and checking in test modechecked 6 September 2026
- AffiliateRail docs, Server-side attribution for Next.js: cookies() from next/headers in a server action, the decode and parse, and the conditional metadatachecked 6 September 2026
- AffiliateRail docs, Connecting Paddle: custom_data.rail_referral on the transaction, filled by the script through Paddle.js or set server-side, and copied by Paddle to the subscription and every renewalchecked 6 September 2026