# Remix

URL: https://supapulse.app/docs/guides/remix

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

## Install [#install]

```bash
npm install @supapulse/core @supapulse/react
```

## Root route [#root-route]

```tsx title="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 [#events]

```tsx title="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 [#revenue-with-stripe]

Identify the customer in the browser, add the Supapulse endpoint in the Stripe dashboard ([Stripe guide](https://supapulse.app/docs/guides/stripe)), and put your user ID in the checkout metadata. Supapulse receives the payments from Stripe itself, so there is no webhook route to write.

```tsx title="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>
  );
}
```

```ts title="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](https://supapulse.app/docs/react/hooks#useidentify).

## Troubleshooting [#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.
