# See which campaign brings your TanStack buyers

> Add MIRA FIVE to a TanStack Start app with one provider and request middleware, then send paid orders from server functions to see buyers per channel.

URL: https://mirafive.io/for/tanstack-start  
Updated: 2026-09-28

One provider counts every route change, and request middleware hands your server functions a client for the orders that paid.

Package `@mirafive/sdk-tanstack`

## Set up in 5 steps

1. ### Install the packages  
It needs React 18.3 or 19 and Node.js 20 or later, and the `/start` entry needs `@tanstack/react-start` 1.168 or later. The middleware loads the server SDK lazily on the server, so it never reaches the browser bundle.  
```  
npm install @mirafive/sdk-tanstack @mirafive/sdk-react @mirafive/sdk-browser @mirafive/sdk-server  
```
2. ### Register the middleware  
Put the secret key of a server source from **Data → Sources** in `MIRAFIVE_SECRET_KEY`, never with a `VITE_` prefix. Create the middleware once, at module scope, because `createStart()` runs its factory per request.  
```  
import { miraMiddleware } from '@mirafive/sdk-tanstack/start'  
import { createStart } from '@tanstack/react-start'  
const mirafive = miraMiddleware()  
export const startInstance = createStart(() => ({  
  requestMiddleware: [mirafive],  
}))  
```
3. ### Wrap the root route  
Put the website key of your website source in `VITE_MIRAFIVE_KEY` and pass it to the provider, which counts the first page and every TanStack Router navigation. The package does not read `import.meta.env` itself, because Vite only replaces it in your own code.  
```  
import { MiraProvider } from '@mirafive/sdk-tanstack'  
import { createRootRoute, HeadContent, Outlet, Scripts } from '@tanstack/react-router'  
import type { ReactNode } from 'react'  
export const Route = createRootRoute({  
  shellComponent: RootDocument,  
  component: RootComponent,  
})  
function RootComponent() {  
  return (  
    <MiraProvider websiteKey={import.meta.env.VITE_MIRAFIVE_KEY}>  
      <Outlet />  
    </MiraProvider>  
  )  
}  
function RootDocument({ children }: { children: ReactNode }) {  
  return (  
    <html lang="en">  
      <head>  
        <HeadContent />  
      </head>  
      <body>  
        {children}  
        <Scripts />  
      </body>  
    </html>  
  )  
}  
```
4. ### Send the paid order from a server function  
`context.mira` is the server SDK's client, one per process. `track()` only buffers, and the middleware flushes once the response is ready, also when the handler throws.  
```  
import { createServerFn } from '@tanstack/react-start'  
export const confirmPayment = createServerFn({ method: 'POST' })  
  .validator((data: { userId: string }) => data)  
  .handler(async ({ context, data }) => {  
    context.mira.track('order_paid', { userId: data.userId, properties: { revenue: 328, currency: 'EUR' } })  
  })  
```
5. ### Check both sides  
Deploy, open a page and watch **Data → Live**; localhost sends nothing unless you pass `trackLocalhost` to the provider. For the server, call this function once. It resolves with the reason `install_check`, nothing is stored, and you remove it afterwards.  
```  
import { createServerFn } from '@tanstack/react-start'  
export const installCheck = createServerFn({ method: 'POST' }).handler(async ({ context }) => {  
  return context.mira.send([{ name: '$install_check' }])  
})  
```

MIRA FIVE is analytics built around the person. In a TanStack Start app one package covers both sides: a provider with the React hooks for the browser, and request middleware that puts a server client on `context` for server functions and server routes.

## How do I record what matters?

In a component, read the client with `useMira()` and call `track`. `useTrackOnMount(name, properties)` sends one event when a component appears, once also under StrictMode.

```tsx
import { useMira } from '@mirafive/sdk-tanstack'

export function AddToCart() {
  const mira = useMira()

  return <button onClick={() => mira.track('added_to_cart', { product: 'merino-crew' })}>Add to cart</button>
}
```

## How does a paid order reach its channel?

Through the person who paid. Switch the website source to **Full** under **Data → Sources**, then give the provider full mode and the `identity()` plugin:

```tsx
import { identity } from '@mirafive/sdk-browser/identity'
import { MiraProvider } from '@mirafive/sdk-tanstack'
import { Outlet } from '@tanstack/react-router'

function RootComponent() {
  return (
    <MiraProvider websiteKey={import.meta.env.VITE_MIRAFIVE_KEY} mode="full" plugins={[identity()]}>
      <Outlet />
    </MiraProvider>
  )
}
```

In components, `useMira().consent({ statistics: true, experiments: true, targeting: false })` passes your banner’s answer, `identify(user.id, { plan: user.plan })` follows sign-in and `reset()` follows sign-out. Before an answer, full mode stores and sends nothing. The server function sends the order with the same user id, so the purchase lands on that person, next to the campaign that first brought them. Create a purchase goal for `order_paid`, and Acquisition shows buyers and revenue per channel and campaign.

## How do I make a webhook count once?

`send()` delivers at once, and a repeat with the same idempotency key is stored once:

```ts
await context.mira.send(
  [{ name: 'order_paid', userId: order.customerId, properties: { revenue: 328, currency: 'EUR' } }],
  { idempotencyKey: `order-${order.id}` },
)
```

The source’s Install tab has **Let your coding agent install it**, a prompt for Claude Code, Cursor or Codex with every step and the check. The [TanStack Start SDK docs](https://docs.mirafive.io/sdks/tanstack-start) cover Workers, Vercel and TanStack Router without Start, and the [setup page](https://mirafive.io/setup) shows who does what, in order.

## Other setups

The same numbers, whichever way your site is built.

- [Astro](https://mirafive.io/for/astro)
- [Next.js](https://mirafive.io/for/nextjs)
- [React](https://mirafive.io/for/react)
- [Laravel](https://mirafive.io/for/laravel)
- [Nuxt](https://mirafive.io/for/nuxt)
- [Symfony](https://mirafive.io/for/symfony)
- [PHP](https://mirafive.io/for/php)
- [Vue](https://mirafive.io/for/vue)
- [Node.js](https://mirafive.io/for/nodejs)
- [Convex](https://mirafive.io/for/convex)
- [JavaScript](https://mirafive.io/for/javascript)
- [any website](https://mirafive.io/for/website)

[Read the docs](https://docs.mirafive.io)

## Questions and answers

### Why are server events lost on Cloudflare Workers or Vercel?

The response ends before the delivery. Pass the platform's `waitUntil` to `miraMiddleware({ waitUntil })`, from `cloudflare:workers` or `@vercel/functions`. On Node.js the process keeps running and the flush completes on its own.

### Can I use context.mira in a route loader?

No. Route loaders are isomorphic, so call a server function from them instead. Server functions and server routes both see `context.mira`.

### Does it work with TanStack Router without Start?

Yes. A single-page app needs `@mirafive/sdk-tanstack`, `@mirafive/sdk-react` and `@mirafive/sdk-browser`. Wrap `RouterProvider` in `MiraProvider`, and pageviews and `useMira()` work the same.

### Why does nothing arrive from the browser?

Localhost sends nothing by default. `VITE_MIRAFIVE_KEY` must be set before the build, because Vite inlines it then; without it the console shows `[mirafive] no website key`.

## See which channel brings buyers

Free for 25,000 events a month. No card needed.

[Start free](https://app.mirafive.io/register)[See pricing](https://mirafive.io/pricing)
