SaaS Referral Program with Rewardful | Coding Capybaras
How to add a SaaS referral program with Rewardful to a Next.js app: the tracking script, passing the referral UUID to Stripe, and when to skip affiliates.
· Justin Boggs

Photo by LinkedIn Sales Solutions on Unsplash
Adding a referral program to your SaaS with Rewardful takes about two hours of real work: paste a tracking script into your root layout, capture the referral UUID that Rewardful drops in the browser, and hand that UUID to Stripe when you create the customer. That's the whole integration. The hard part isn't the code — it's deciding whether you should run an affiliate program at all, because Rewardful starts at $49/month whether your affiliates send you $10,000 or $0. This walkthrough covers the exact Next.js wiring, the App Router gotcha nobody warns you about, and the honest math on when a referral program is worth paying for.
TL;DR
- A referral program pays third parties a commission for customers they send you — Rewardful is the layer that tracks who sent whom and calculates what you owe.
- The Next.js integration is two moving parts: the tracking script in your root layout, and the referral UUID passed to Stripe as customer metadata (server-side) or
client_reference_id(Checkout).- Rewardful is Stripe- and Paddle-only, starts at $49/month for up to $7,500/mo in affiliate revenue, and charges 0% transaction fees on every plan.
- Don't wire this in before you have an audience worth recruiting from. A referral program with no affiliates is a $49/month line item that does nothing.
What does a referral program actually do for an indie SaaS?
A referral program pays a commission to people who send you paying customers, and referral software is the plumbing that attributes each sale to the right referrer and calculates the payout. That's it. It's not growth strategy. It's accounting infrastructure for a growth strategy you have to build separately.
This distinction matters because I see founders install Rewardful the week they launch, expecting affiliates to materialize. They don't. The software tracks referrals; it doesn't create them. If nobody currently recommends your product to their audience, adding tracking to your app changes nothing except your monthly burn.
Here's when it does work. You have customers who already talk about you unprompted. You have a niche newsletter or YouTube channel that covers your category. You sell to a market where consultants and agencies recommend tools to clients. In all three cases, the recommendation was going to happen anyway — the referral program just gives those people a reason to do it more, and gives you a way to find out that it happened.
The mechanics are boring and worth understanding before you write any code:
- An affiliate gets a link like
yoursite.com/?via=justin. - A visitor clicks it. Rewardful's script sees the
viaparameter, creates a referral record, and stores its UUID in a first-party cookie on your domain. - The visitor browses. Maybe they sign up today, maybe in three weeks — the cookie carries the attribution.
- They pay. Your code passes the referral UUID to Stripe.
- Rewardful watches your Stripe account, matches the UUID to the affiliate, and books a commission.
Step 4 is the only step that's your problem. Everything else is Rewardful's script and Rewardful's Stripe sync. Which is why the integration is genuinely small — and why the failure mode is so consistent: when referrals don't track, it's almost always because step 4 didn't happen.
One thing worth flagging up front, because it shapes the whole decision: Rewardful only works with Stripe or Paddle. Their pricing page FAQ is blunt about it — "We currently support only Stripe or Paddle." If you're on Lemon Squeezy or a custom processor, stop reading and look elsewhere. If you're on the standard Next.js + Supabase + Stripe + Resend stack, you're fine.
How much does Rewardful cost, and when is it worth it?
Rewardful's published pricing is three tiers, gated on how much revenue your affiliates produce — not on how many affiliates you have or how much traffic they send:
| Plan | Price | Affiliate revenue cap | Notable limits | | --- | --- | --- | --- | | Starter | $49/mo | Up to $7,500/mo | 1 campaign, up to 2 team members | | Growth | $99/mo | Up to $15,000/mo | Unlimited campaigns, branded portal, custom domain | | Enterprise | $149+/mo | Over $15,000/mo | Adds phone support, one-click PayPal payouts |
Every plan includes 0% transaction fees, unlimited affiliates, unlimited visitors, REST API access, PayPal and Wise payouts, self-referral fraud detection, and coupon code tracking. There's a 14-day free trial, two months free on annual billing, and — per their FAQ — a no-questions-asked refund if you contact them within 30 days of a monthly plan.
The 0% transaction fee is the part worth pausing on. Several competitors charge a percentage of affiliate revenue on top of a monthly fee, which means your bill scales with your success. Rewardful's flat fee means the opposite: it's brutally expensive at low volume and cheap at high volume.

Run the numbers on yourself before you sign up. At $1,000/month in affiliate-driven revenue, Starter costs you 4.9% of that revenue — on top of whatever commission you're paying the affiliate. Stack a 20% commission on that and 25% of every referred dollar is gone before Stripe takes its cut. At $7,500/month, the same $49 is 0.65% and the tool has essentially paid for itself twice over by lunch.
So the honest threshold: if you can't picture affiliates producing at least $1,000–$2,000/month within a quarter, the software isn't the bottleneck and $49/month is a tax on optimism. Build the audience first. Getting to your first 100 customers almost never happens through affiliates, because affiliates promote products that already have proof.
How do you add the Rewardful tracking script to Next.js?
Rewardful's script has two pieces, and the order matters. First the queue shim, then the async loader. From Rewardful's JavaScript API docs:
<script>(function(w,r){w._rwq=r;w[r]=w[r]||function(){(w[r].q=w[r].q||[]).push(arguments)}})(window,'rewardful');</script>
<script async src='https://r.wdfl.co/rw.js' data-rewardful='YOUR-API-KEY'></script>
The first line creates a rewardful() function that queues calls until the real script loads. The second line loads the real script asynchronously. If you swap the order, your queued calls vanish.
In the App Router, both go in your root layout so they run on every page — marketing site and app. Rewardful's docs are explicit that the script "must appear on every page of your application and marketing website," which matters if your landing pages and your app are in different route groups. If your landing pages and your app share a root layout, this is free; if they don't, you need the script in both.
Use next/script with the right strategy:
// app/layout.tsx
import Script from 'next/script'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Script id="rewardful-queue" strategy="beforeInteractive">
{`(function(w,r){w._rwq=r;w[r]=w[r]||function(){(w[r].q=w[r].q||[]).push(arguments)}})(window,'rewardful');`}
</Script>
<Script
id="rewardful-loader"
src="https://r.wdfl.co/rw.js"
data-rewardful={process.env.NEXT_PUBLIC_REWARDFUL_API_KEY}
strategy="afterInteractive"
/>
{children}
</body>
</html>
)
}
Two notes. The queue shim uses beforeInteractive so it exists before anything tries to call rewardful(). The API key goes in NEXT_PUBLIC_REWARDFUL_API_KEY — it's a public identifier that ships to the browser regardless, so the NEXT_PUBLIC_ prefix is correct here. That's the exception, not the rule: everything else stays server-side in .env.local.
Now the part that bites App Router users. Rewardful auto-injects a hidden referral input into any form with a data-rewardful attribute — but only for forms present in the DOM at page load. Client-rendered forms, modals, and anything mounted after hydration get missed. Their docs cover this: call Rewardful.Forms.attach() after the form hits the DOM.
In React, I skip the form-injection mechanism entirely and read the value directly. It's less magic and it survives re-renders:
'use client'
import { useEffect, useState } from 'react'
export function useRewardfulReferral() {
const [referral, setReferral] = useState<string | null>(null)
useEffect(() => {
const timeout = setTimeout(() => setReferral(null), 2000) // don't block on a script that never loads
window.rewardful?.('ready', () => {
clearTimeout(timeout)
setReferral(window.Rewardful?.referral || null)
})
return () => clearTimeout(timeout)
}, [])
return referral
}
That setTimeout is deliberate. Rewardful's own docs warn: "Do not run critical code within a ready handler" — if the script is blocked by an ad blocker or a network hiccup, ready never fires. Your checkout button must not depend on it. A missed commission is an annoyance; a checkout that silently never fires is a dead business.
How do you pass the referral to Stripe?
This is step 4 — the only step that's actually yours. There are two paths depending on how you take payment, and picking the wrong one is the most common reason referrals don't track.
If you use Stripe Checkout, pass the referral UUID as clientReferenceId. Rewardful's Stripe Checkout integration guide shows the client-side shape:
if (window.Rewardful && window.Rewardful.referral) {
checkoutParams.clientReferenceId = window.Rewardful.referral
}
In a modern Next.js app you're probably creating the Checkout session server-side instead, so pass the referral from your client component to your server action and set client_reference_id there:
// server action
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
client_reference_id: referral ?? undefined, // the Rewardful UUID
success_url: `${origin}/dashboard?checkout=success`,
cancel_url: `${origin}/pricing`,
})
If you create Stripe customers yourself, attach the UUID as customer metadata instead. Rewardful's custom Stripe integration guide has the Node shape:
const customerParams = { email: req.body.email }
if (req.body.referral) {
customerParams.metadata = { referral: req.body.referral }
}
stripe.customers.create(customerParams)
Adding that metadata key is what converts the referral. From that point, every charge against that customer books a commission for the affiliate, and Rewardful adjusts automatically for upgrades, downgrades, trials, cancellations, and refunds. You don't write reconciliation logic. That's most of what you're paying for.
A few things I learned the annoying way:
- The metadata key must be exactly
referral. Notreferral_id, notrewardful_referral. Rewardful looks for that literal key. - If the customer already exists, update them rather than skipping — Rewardful's docs point at the update-customer endpoint for exactly this.
- Test in an incognito window. Every troubleshooting list Rewardful publishes says this, because your own cookie state will lie to you.
- There's a client-side-only path using
rewardful('convert', { email })on your thank-you page, which matches the most recent Stripe customer with that email. It requires read-write Stripe permission and only matches customers created in the last 24 hours. It's a legitimate escape hatch for hosted checkouts you can't modify — but if you control your server, use metadata.
If referrals still aren't tracking, the debug order that works: confirm the script loads at all (check the network tab for rw.js), then check Rewardful.referral returns a UUID in the console after visiting with ?via=sometoken, then confirm the UUID reaches Stripe by looking at the customer object in your Stripe dashboard. The break is almost always at the last step. Same discipline as debugging Stripe webhooks — verify each hop instead of guessing.
What are the alternatives, and when should you skip this entirely?
Rewardful isn't the only option, and it's not the right one for everyone.
| | Rewardful | Build it yourself | Do nothing | | --- | --- | --- | --- | | Cost | $49–$149+/mo | Your time, forever | $0 | | Processors | Stripe, Paddle only | Whatever you support | n/a | | Affiliate dashboard | Included | You build it | n/a | | Payouts | PayPal / Wise built in | You wire it up | n/a | | Handles refunds/downgrades | Automatically | You handle every edge case | n/a | | Right when | Affiliates already exist | Novel commission logic | Pre-audience |
The build-it-yourself column is more tempting than it should be. Tracking a ?via= param and storing it is a two-hour job. What takes months is everything after: an affiliate-facing dashboard, payout batching across countries, commission clawbacks when a referred customer refunds in month two, self-referral fraud detection, and the tax paperwork. This is the clearest case of buying rather than building I've run into — the visible part is trivial and the invisible part is enormous.
The "do nothing" column deserves more respect than founders give it. Most indie SaaS products should not have an affiliate program in year one. You're better off spending that $49 and those two hours on the thing that actually moves early revenue: talking to customers, writing content that ranks, or shipping the feature three people asked for last week.
The signal I'd wait for: someone recommends your product publicly, without being asked, to an audience that isn't yours. When that happens twice, wire up Rewardful and email both of them. Before that, you're building a payout system for a queue of zero.
One more honest tradeoff. A referral program changes how you price. If you're paying 20–30% recurring commission, that comes out of the same margin funding your support time and your infrastructure. If you're running thin, model it before you launch it — the subscription billing math gets ugly fast when you stack a commission on top of a discount you already offered.
Frequently asked questions
Does Rewardful work with Next.js App Router?
Yes. Put the queue shim and the loader script in your root layout via next/script, using beforeInteractive for the shim and afterInteractive for the loader. The only App Router-specific gotcha is that Rewardful's automatic form injection misses client-rendered forms — read Rewardful.referral directly in a ready handler instead.
Do I need client_reference_id or customer metadata?
Use client_reference_id if you're using Stripe Checkout, and customer metadata with the key referral if you create Stripe customers yourself. Both convert the referral; they're just two different places Rewardful looks. Don't do both.
What happens if the Rewardful script is blocked by an ad blocker?
The referral simply isn't tracked and no commission is booked. That's an acceptable loss. What's not acceptable is your checkout depending on the script — never put payment code inside a rewardful('ready') handler without a timeout fallback, per Rewardful's own warning.
Can I use Rewardful without Stripe?
Only if you're on Paddle. Rewardful's pricing FAQ states they support Stripe and Paddle exclusively. If you're on Lemon Squeezy, Chargebee, or a custom processor, you need a different tool — Lemon Squeezy has affiliate features built in, which is a real point in its favor.
How long does the referral cookie last?
Rewardful stores the referral in a first-party cookie on your own domain — which is why their links use ?via=token on your root domain rather than redirecting through a third-party subdomain. Their pricing page calls this out directly: "No ugly redirects or 3rd party subdomains." Check your campaign settings in the dashboard for the exact window, since that's where cookie length is set.
Should I offer a coupon to referred customers too?
Rewardful supports double-sided incentives — the affiliate earns a commission and the referred customer gets a discount. It converts better, but you're now paying twice per referral. Model it against your margin before turning it on.
The short version
A referral program with Rewardful is a two-hour Next.js integration: the tracking script in your root layout, and the referral UUID passed to Stripe as client_reference_id or customer metadata. The code isn't the hard part, and if you're stuck it's almost certainly because the UUID never reached Stripe. The hard part is being honest about whether you have anyone to recruit as an affiliate yet. At $49/month against $1,000 in affiliate revenue you're burning 5% of that revenue on tooling before commissions; at $7,500 you're at 0.65% and it's obviously worth it. Wait for the second unprompted public recommendation, then wire it up.
If you're building a SaaS with AI coding tools, Coding Capybaras is the free boilerplate I built for exactly this workflow — the marketplace has copy-paste prompts for wiring integrations like this one into a Next.js + Supabase + Stripe app.