supapulsedocs
Integration guidesRemix

Remix

Add Supapulse to a Remix app, track events, and attribute Stripe revenue through an action and a resource route.

Install

npm install @supapulse/core @supapulse/react

Root route

app/root.tsx
import { SupapulseProvider } from '@supapulse/react';
import { json } from '@remix-run/node';
import { Links, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from '@remix-run/react';
import { useMemo } from 'react';

export async function loader() {
  return json({ supapulseToken: process.env.SUPAPULSE_PUBLIC_TOKEN! });
}

export default function App() {
  const { supapulseToken } = useLoaderData<typeof loader>();
  const config = useMemo(() => ({ apiToken: supapulseToken }), [supapulseToken]);

  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <SupapulseProvider config={config}>
          <Outlet />
        </SupapulseProvider>
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

Remix navigates with the History API, so every transition is a pageview.

Events

app/routes/pricing.tsx
import { useEvent } from '@supapulse/react';

export default function Pricing() {
  const trackEvent = useEvent();
  return <button onClick={() => trackEvent('plan_selected', { plan: 'growth' })}>Choose Growth</button>;
}

Revenue with Stripe

Identify the customer in the browser, add the Supapulse endpoint in the Stripe dashboard (Stripe guide), and put your user ID in the checkout metadata. Supapulse receives the payments from Stripe itself, so there is no webhook route to write.

app/routes/pricing.tsx
import { useSupapulse } from '@supapulse/react';
import { Form } from '@remix-run/react';

export function BuyForm({ priceId }: { priceId: string }) {
  const supapulse = useSupapulse();

  async function addVisitor(event: React.FormEvent<HTMLFormElement>) {
    const form = event.currentTarget;
    form.elements.namedItem('fingerprint')?.setAttribute('value', (await supapulse.getVisitorFingerprint()) ?? '');
    form.elements.namedItem('sessionId')?.setAttribute('value', supapulse.getSessionId());
  }

  return (
    <Form method="post" action="/checkout" onSubmit={addVisitor}>
      <input type="hidden" name="priceId" value={priceId} />
      <input type="hidden" name="fingerprint" />
      <input type="hidden" name="sessionId" />
      <button>Buy</button>
    </Form>
  );
}
app/routes/checkout.ts
import { redirect, type ActionFunctionArgs } from '@remix-run/node';
import Stripe from 'stripe';

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

export async function action({ request }: ActionFunctionArgs) {
  const form = await request.formData();
  const user = await requireUser(request); // your auth
  const metadata = {
    supapulse_customer_id: user.id,
    supapulse_visitor_fingerprint: String(form.get('fingerprint') ?? ''),
    supapulse_session_id: String(form.get('sessionId') ?? ''),
  };

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [{ price: String(form.get('priceId')), quantity: 1 }],
    success_url: 'https://acme.com/thanks',
    metadata,
    subscription_data: { metadata },
  });
  return redirect(session.url!);
}

Call identify(user.id) from useIdentify() once the user is signed in, after the provider is ready. See Hooks.

Troubleshooting

  • The hidden inputs are empty. The form fills them in an onSubmit handler, so they only have values once the tracker has initialized after hydration. Without them Supapulse still attributes through identify() and supapulse_customer_id.
  • No pageviews on transitions. Make sure the provider wraps <Outlet /> in root.tsx, not a nested route that remounts.