Remix Integration
Remix renders on the server first, then hydrates on the client. Follow this guide to ensure Pulsora initializes only in the browser and that you capture revenue metadata.
Install Dependencies
npm install @pulsora/react @pulsora/core
Create environment variables:
PULSORA_PUBLIC_TOKEN=pub_123
PULSORA_SECRET_TOKEN=sec_123
Expose the public token to the client via loader.
Root Layout
// app/root.tsx
import type { LinksFunction, LoaderFunctionArgs } from '@remix-run/node';
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
useLocation,
} from '@remix-run/react';
import { PulsoraProvider, usePageview } from '@pulsora/react';
export const loader = async ({ request }: LoaderFunctionArgs) => {
return {
pulsoraToken: process.env.PULSORA_PUBLIC_TOKEN,
};
};
function Analytics() {
const location = useLocation();
usePageview({ trigger: location.pathname + location.search });
return null;
}
export default function App() {
const { pulsoraToken } = useLoaderData<typeof loader>();
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<PulsoraProvider config={{ apiToken: pulsoraToken }}>
<Analytics />
<Outlet />
</PulsoraProvider>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
Tracking Events
// app/routes/pricing.tsx
import { useEvent } from '@pulsora/react';
export default function Pricing() {
const trackEvent = useEvent();
return (
<button
className="btn btn-primary"
onClick={() => trackEvent('cta_click', { plan: 'pro' })}
>
Start Trial
</button>
);
}
Revenue Attribution with Stripe
// app/routes/checkout.tsx (action handler)
import type { ActionFunctionArgs } from '@remix-run/node';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});
export const action = async ({ request }: ActionFunctionArgs) => {
const { priceId, visitor_fingerprint, session_id } = await request.json();
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: 'https://app.example.com/success',
cancel_url: 'https://app.example.com/cancel',
metadata: {
visitor_fingerprint,
session_id,
},
});
return { sessionId: session.id };
};
// app/routes/api.stripe-webhook.ts
import type { ActionFunctionArgs } from '@remix-run/node';
import { Revenue } from '@pulsora/revenue';
const revenue = new Revenue({
apiToken: process.env.PULSORA_SECRET_TOKEN!,
});
export const action = async ({ request }: ActionFunctionArgs) => {
const payload = await request.json();
if (payload.type === 'checkout.session.completed') {
const session = payload.data.object;
await revenue.track({
visitor_fingerprint: session.metadata.visitor_fingerprint,
session_id: session.metadata.session_id,
customer_id: session.customer,
amount: session.amount_total / 100,
currency: session.currency.toUpperCase(),
transaction_id: session.id,
payment_processor: 'stripe',
event_type: 'subscription',
customer_subscription_id: session.subscription,
is_recurring: true,
});
}
return { received: true };
};
Troubleshooting
- Hydration errors — Ensure Pulsora code sits inside the provider and runs only in the browser.
- Undefined token — Confirm your loader exposes
PULSORA_PUBLIC_TOKENbefore hydration. - Revenue validation errors — Verify Stripe metadata includes
visitor_fingerprintandsession_id.
Looking for more patterns? Check the generic custom integration guide →