# Count only the orders that committed

> Record MIRA FIVE events and paid orders from Convex mutations with @mirafive/sdk-convex, sent only after the transaction commits and stored once.

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

A mutation schedules the delivery instead of sending it, so an order that rolled back never reaches your numbers.

Package `@mirafive/sdk-convex`

## Set up in 5 steps

1. ### Install the packages  
Delivery goes through `@mirafive/sdk-server`. It needs `convex` 1.25 or newer and runs in Convex's default runtime, so you add no `"use node"`.  
```  
npm install @mirafive/sdk-convex @mirafive/sdk-server  
```
2. ### Set the secret key on the deployment  
Use the secret key of a server source from **Data → Sources**. It lives in the deployment's environment, never in code or a client bundle. A missing key never breaks the deployment; events are dropped with one warning.  
```  
npx convex env set MIRAFIVE_SECRET_KEY mf_…  
```
3. ### Create the client  
Export the client and its delivery action from one module. Keep the `: MiraConvex` annotation; without it TypeScript reports TS7022.  
```  
import { MiraConvex } from '@mirafive/sdk-convex'  
import { internal } from './_generated/api'  
export const mira: MiraConvex = new MiraConvex({  
  key: process.env.MIRAFIVE_SECRET_KEY,  
  deliver: internal.mirafive.deliver,  
})  
export const deliver = mira.deliverAction()  
```
4. ### Track the paid order in the mutation  
`track()` schedules `deliver`, and Convex runs it only if the mutation commits. The idempotency key makes a mutation that runs twice for the same order count once. `await` every call.  
```  
import { v } from 'convex/values'  
import { mutation } from './_generated/server'  
import { mira } from './mirafive'  
export const pay = mutation({  
  args: { orderId: v.id('orders') },  
  handler: async (ctx, { orderId }) => {  
    const order = await ctx.db.get(orderId)  
    if (!order) {  
      throw new Error('order not found')  
    }  
    await ctx.db.patch(orderId, { paidAt: Date.now() })  
    await mira.track(ctx, 'order_paid', {  
      userId: order.customerId,  
      properties: { revenue: 328, currency: 'EUR' },  
      idempotencyKey: `order-${orderId}`,  
    })  
  },  
})  
```
5. ### Check the delivery  
Run the delivery action once with an install check, which is never stored or billed. It prints `null` when MIRA FIVE accepted the batch. Then run a mutation that tracks and find the event under **Data → Live**; the Convex dashboard shows the `mirafive:deliver` run.  
```  
npx convex run mirafive:deliver '{"events":"[{\"name\":\"$install_check\"}]","idempotencyKey":"install-check"}'  
```

MIRA FIVE is analytics built around the person. `@mirafive/sdk-convex` records events from Convex mutations and actions and delivers them through `@mirafive/sdk-server`, so a purchase is counted where your app decides the order is paid, and only if that decision is stored.

## How does a paid order reach its channel?

Through the person who paid. The browser side of your app uses the [browser SDK](https://mirafive.io/for/javascript) or your framework’s package, such as [React](https://mirafive.io/for/react). With consent, the browser keeps an anonymous id; pass it to a mutation, and `identify` links the visitor’s earlier events to your user id:

```ts
await mira.identify(ctx, userId, { plan: 'pro' }, { anonymousId })
```

Use your internal user id, never an email address. The paid order carries the same `userId`, so MIRA FIVE puts it on that person, next to the channel that first brought them. Create a purchase goal for `order_paid` to see buyers and revenue per channel and campaign.

## How are repeats counted?

Every delivery carries an idempotency key, fixed when it is queued: yours, or a fresh UUID. A rerun of the action sends the byte-identical batch, and MIRA FIVE stores it once. Pass your own key when a mutation can run twice for the same thing, such as a webhook.

Several events from one mutation go out as one delivery with `trackMany()`:

```ts
await mira.trackMany(ctx, [
  { name: 'order_paid', userId, properties: { revenue: 328, currency: 'EUR' } },
  { name: 'invoice_created', userId, properties: { invoiceId } },
])
```

Server events are sent in full mode by default, and your app holds the consent or other lawful basis for the ids. With `mode: 'consentless'`, events carry no identifiers.

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; for a server source it never contains the secret key. The [Convex SDK docs](https://docs.mirafive.io/sdks/convex) cover the outbox and workpool retries, 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)
- [TanStack Start](https://mirafive.io/for/tanstack-start)
- [Node.js](https://mirafive.io/for/nodejs)
- [JavaScript](https://mirafive.io/for/javascript)
- [any website](https://mirafive.io/for/website)

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

## Questions and answers

### Can a rolled-back mutation count an order?

No. A mutation cannot reach the network, so `track()` schedules the delivery, and Convex runs a scheduled function only if the mutation commits.

### When should I turn on the outbox?

Above roughly one event per second. With `outbox` on, mutations write events to a `miraOutbox` table and a cron sends them in batches of up to 1,000\. That saves Convex action time, not MIRA FIVE usage.

### Does the scheduler retry a failed delivery?

No. Hand deliveries to a Convex workpool through the `enqueue` option. The idempotency key is fixed when a delivery is queued, so a retried delivery is stored once.

### Can I track from a query?

No, queries cannot schedule. `track()` works in mutations and actions, and in an action it schedules a delivery as well.

## 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)
