Refport
Quickstarts

TanStack Start

Track referral clicks, leads, and sales in a TanStack Start (Vite + Nitro) project.

Prerequisites

  • TanStack Start with Vinxi / Nitro
  • A Refport account and API key (Settings → API Keys)
  • A referral program created in the dashboard

Install the React SDK and add <RefportTracker /> to your root route. It reads the refp_id query parameter and persists it in a cookie automatically.

npm install @refport/react
src/routes/__root.tsx
import { RefportTracker } from '@refport/react';
import { Outlet, createRootRoute } from '@tanstack/react-router';

export const Route = createRootRoute({
  component: RootComponent,
});

function RootComponent() {
  return (
    <>
      <RefportTracker />
      <Outlet />
    </>
  );
}

Install the Node.js SDK and create a shared singleton.

npm install refport
src/lib/refport.ts
import { Refport } from 'refport';

export const refport = new Refport({
  apiKey: process.env.REFPORT_API_KEY!,
});

Add REFPORT_API_KEY to your .env.

Use createServerFn and read the cookie via getRequestHeaders() from @tanstack/react-start/server.

src/functions/auth.ts
import { getRequestHeaders } from '@tanstack/react-start/server';
import { createServerFn } from '@tanstack/react-start';
import { getClickIdFromCookie } from 'refport';
import { refport } from '~/lib/refport';
import { z } from 'zod';

export const signUpFn = createServerFn({ method: 'POST' })
  .validator(z.object({ email: z.string().email(), password: z.string().min(8) }))
  .handler(async ({ data }) => {
    const user = await createUser(data);

    try {
      const headers = getRequestHeaders();
      const clickId = getClickIdFromCookie(headers.get('cookie'));
      if (clickId) {
        await refport.track.lead({
          clickId,
          eventName: 'Sign Up',
          customerExternalId: user.id,
          customerEmail: user.email,
          customerName: user.name,
        });
      }
    } catch {
      /* tracking failure must never block sign-up */
    }

    return user;
  });

Using Better Auth? The @refport/better-auth plugin does this automatically — skip this step.

Create a Vinxi API route for your payment webhook and use getClickIdFromRequest() with the Fetch API Request object.

src/routes/api/webhooks/payment.ts
import { json } from '@tanstack/react-start';
import { createAPIFileRoute } from '@tanstack/react-start/api';
import { getClickIdFromRequest } from 'refport';
import { refport } from '~/lib/refport';

export const APIRoute = createAPIFileRoute('/api/webhooks/payment')({
  POST: async ({ request }) => {
    const order = await request.json();

    try {
      const clickId = getClickIdFromRequest(request);
      if (clickId) {
        await refport.track.sale({
          clickId,
          customerExternalId: order.userId,
          amount: order.amountCents,
          currency: 'usd',
          eventName: 'Purchase',
          invoiceId: order.invoiceId,
          paymentProcessor: 'stripe',
        });
      }
    } catch {
      /* tracking failure must never block order processing */
    }

    return json({ ok: true });
  },
});

If you create Stripe Checkout sessions in your own code, pass the click ID and customer ID in the session metadata so Refport's webhook handler can attribute the payment to the referral.

src/routes/api/checkout.ts
import { json } from '@tanstack/react-start';
import { createAPIFileRoute } from '@tanstack/react-start/api';
import { getClickIdFromRequest } from 'refport';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export const APIRoute = createAPIFileRoute('/api/checkout')({
  POST: async ({ request }) => {
    const clickId = getClickIdFromRequest(request);

    const session = await stripe.checkout.sessions.create({
      mode: 'subscription',
      line_items: [{ price: 'price_xxx', quantity: 1 }],
      success_url: 'https://yoursite.com/success',
      cancel_url: 'https://yoursite.com/pricing',
      metadata: {
        refportExternalCustomerId: currentUser.id,
        refportClickId: clickId ?? undefined,
      },
    });

    return json({ url: session.url });
  },
});

See the Stripe integration guide for the full attribution fallback chain and alternative approaches.

What's next

On this page