@mirafive/sdk-tanstack
v1.0.0
Published
MIRA FIVE for TanStack Start and TanStack Router: a client provider with flag hooks, and request middleware for server events and flags.
Maintainers
Readme
@mirafive/sdk-tanstack
MIRA FIVE for TanStack Start and TanStack Router: a client provider with feature-flag
hooks, and request middleware that puts server events and flags on context and sends
them once the response is ready. Privacy-first analytics and feature flags from MIRA FIVE,
hosted in the EU.
Size
| Import | min + gzip |
|---|---|
| @mirafive/sdk-tanstack (client) | 0.77 kB |
| @mirafive/sdk-tanstack + @mirafive/sdk-react | 1.29 kB |
| @mirafive/sdk-tanstack/start | 0.75 kB |
Measured with the peers external (react, @tanstack/react-start,
@mirafive/sdk-browser, @mirafive/sdk-server, and @mirafive/sdk-react in the first
row): these are the bytes this package adds. The browser SDK core with pageviews is
2.31 kB on top. The middleware loads @mirafive/sdk-server lazily on the server, so it
never reaches the browser bundle, although src/start.ts is bundled for both. What you
do not import is not shipped (sideEffects: false).
Install
npm install @mirafive/sdk-tanstack @mirafive/sdk-react @mirafive/sdk-browser @mirafive/sdk-server
# or: bun add / pnpm add / yarn addPeers: react ≥ 18.3, @mirafive/sdk-react and @mirafive/sdk-browser ^1.0.0; for
/start also @tanstack/react-start ≥ 1.168 and @mirafive/sdk-server ^1.0.0. A
TanStack Router SPA without Start needs only the first three.
Quickstart
.env:
VITE_MIRAFIVE_KEY=mf_… # the source's website key, public
MIRAFIVE_SECRET_KEY=mf_… # the source's secret key, server only (never VITE_-prefixed)// src/start.ts
import { miraMiddleware } from "@mirafive/sdk-tanstack/start"
import { createStart } from "@tanstack/react-start"
// Created once, outside the factory: createStart() runs its factory per request.
const mirafive = miraMiddleware()
export const startInstance = createStart(() => ({
requestMiddleware: [mirafive]
}))// src/flags.ts: server functions get context.mira and context.flagsFor
import { createServerFn } from "@tanstack/react-start"
export const getFlagBootstrap = createServerFn({ method: "GET" }).handler(async ({ context }) => {
const flags = await context.flagsFor({ userId: undefined }) // your own pseudonymous id, if signed in
return flags.bootstrap()
})
export const trackSignup = createServerFn({ method: "POST" })
.validator((data: { userId: string }) => data)
.handler(async ({ context, data }) => {
context.mira.track("signup", { userId: data.userId, properties: { plan: "pro" } })
})// src/routes/__root.tsx
import { flags } from "@mirafive/sdk-browser/flags"
import { MiraProvider } from "@mirafive/sdk-tanstack"
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router"
import { getFlagBootstrap } from "../flags"
export const Route = createRootRoute({
loader: () => getFlagBootstrap(),
staleTime: Infinity, // the bootstrap only matters for the first server render
shellComponent: ({ children }) => (
<html lang="en">
<head><HeadContent /></head>
<body>{children}<Scripts /></body>
</html>
),
component: function Root() {
const bootstrap = Route.useLoaderData()
// Renders the mirafive-flags block and hands the same answers to the flag hooks.
return (
<MiraProvider websiteKey={import.meta.env.VITE_MIRAFIVE_KEY} bootstrap={bootstrap} plugins={[flags()]}>
<Outlet />
</MiraProvider>
)
}
})// any component
import { useFlag, useFlagConfig, useMira, useTrackOnMount } from "@mirafive/sdk-tanstack"
function Checkout() {
const newCheckout = useFlag("new-checkout", false)
const { max } = useFlagConfig("limits", { max: 1 })
const mira = useMira()
useTrackOnMount("checkout_viewed")
return <button onClick={() => mira.track("buy_clicked")}>{newCheckout === true ? `Buy up to ${max}` : "Buy"}</button>
}Analytics only, or a TanStack Router SPA without Start: render
<MiraProvider websiteKey={import.meta.env.VITE_MIRAFIVE_KEY}> around the app and skip
the rest. Pageviews are counted on every navigation.
A working Start app lives in examples/start (bun run example packs
the SDKs, installs them as npm would, and runs vite build).
Verify it: open a page on a deployed host (not localhost) and look for
POST https://events.mirafive.io/v1/batch/mf_… answering 202 in the network tab; the
pageview then shows in the source's live view. Server side, a server function running
await context.mira.send([{ name: "$install_check" }]) resolves to
{ accepted: 0, dropped: 1, reason: "install_check" } when the secret key and host work.
Consent & privacy
- Default mode:
consentlessin the browser. No cookies, no storage, no ids; it needs no consent banner.mode="full"adds an anonymous id, a session id and your user id; it needsidentity()inpluginsand a consent answer (useMira().consent({ statistics, experiments, targeting })) from your consent manager. Before an answer nothing is stored. - Server events (
context.mira) default tofullmode: you decide the lawful basis for the ids you send. - Do Not Track, Global Privacy Control,
window.__mirafive_ignoreand prerendering send nothing from the browser. On the server,context.flagsFor()readsSec-GPC: 1andDNT: 1from the request and passesoptedOut: no ids, no segment lookup, no exposure. - A response whose request read flags gets
Cache-Control: private, no-store, so one visitor's flags never sit in a shared cache. - This package stores nothing. It reads
MIRAFIVE_SECRET_KEYandMIRAFIVE_HOSTon the server and theSec-GPC/DNTrequest headers.
API reference
@mirafive/sdk-tanstack:
<MiraProvider websiteKey host? mode? plugins? flushAt? flushAfterMs? trackLocalhost? bootstrap?>: creates the browser client once, on the first render in the browser, and keeps it for the page's lifetime (later prop changes are ignored, and a changed plugin set is warned about). PasswebsiteKey={import.meta.env.VITE_MIRAFIVE_KEY}.pageviews()is added unlesspluginsalready holds one.bootstrapisflags.bootstrap()or aFlagBootstrap: the provider renders it as themirafive-flagsblock and hands it to the hooks. Without a key it sends nothing and warns once, in every build.useMira(),useFlag(key, fallback),useFlagConfig(key, fallback),useTrackOnMount(name, properties?): re-exported from@mirafive/sdk-react.type MiraProviderProps,type FlagBootstrap.
@mirafive/sdk-tanstack/start:
miraMiddleware({ key?, host?, waitUntil? }): TanStack Start request middleware. PutsmiraandflagsFor(unit?)oncontext. OneMiraand oneMiraFlagsper process for each key (keyorMIRAFIVE_SECRET_KEY) and host (hostorMIRAFIVE_HOST), however often it is called; a failed start is retried by the next request. It flushes when the response is ready, handing the delivery towaitUntilwhen given, and setsCache-Control: private, no-storewhen flags were read.<MiraFlagsScript flags={UserFlags | string} />: the escaped<script type="application/json" id="mirafive-flags">block, for pages whose provider gets nobootstrap. Never together with a providerbootstrap: that renders the block already.type MiraContext({ mira, flagsFor }),type MiraMiddlewareOptions,type FlagUnit,type UserFlags.
Framework / runtime notes
- Why
websiteKey, notkey: React reserves thekeyprop. It is required rather than read fromimport.meta.envinside the package, because Vite only replacesimport.meta.envin your own code. - Hydration: flag hooks render
bootstrapon the server and during hydration, then the browser SDK's answers, re-rendering only when an answer really changes. A bootstrap older than 7 days is ignored, as the browser SDK ignores it. Withoutflags()inplugins, hooks fall back after hydration. - Where context is available: server functions and server routes see
context.miraandcontext.flagsForfrom the global request middleware. Route loaders are isomorphic: read flags through a server function, as above. - Serverless and edge: on Node the process keeps running and the flush completes on
its own. On Cloudflare Workers pass
miraMiddleware({ waitUntil })withwaitUntilfromcloudflare:workers; on Vercel,waitUntilfrom@vercel/functions. Events tracked while a streamed body is still rendering leave with the client's one-second timer, whichwaitUntildoes not cover: track in server functions and routes, not during streaming. - Where the middleware lives: create it once at module scope and put that value in
requestMiddleware. CallingmiraMiddleware()inside thecreateStartfactory also works (the clients are shared), but builds a new middleware per request. - No
process: on runtimes withoutprocess.env, passkeyandhost. - Navigation:
pageviews()counts TanStack Router navigations through the Navigation API or the History API; there is no router subscription to add. - CSP: the bootstrap block is
type="application/json", whichscript-srcdoes not govern. Allowconnect-src https://events.mirafive.io.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Nothing arrives | Local hosts are off by default (trackLocalhost); Do Not Track or GPC is on; VITE_MIRAFIVE_KEY was not set at build time; the origin is not allowed on the source. |
| 403 secret_key_in_path / website_key_as_bearer | The key kinds are swapped: the provider takes the website key, the middleware the secret key. |
| 403 origin_not_allowed | Add the site's origin to the source in MIRA FIVE. |
| A flag always returns its fallback | No flags() in plugins; the flag is not in this source or not marked for the website (a bootstrap carries only those); experiments consent is missing; MIRAFIVE_SECRET_KEY is missing on the server ([mirafive] no key in the logs). |
| context.mira is undefined | miraMiddleware() is not in requestMiddleware in src/start.ts. |
| Server events lost on Workers or Vercel | Pass the platform's waitUntil to miraMiddleware(). |
For AI agents
Copy-paste setup prompt:
Add MIRA FIVE analytics (and feature flags) to this TanStack Start app with @mirafive/sdk-tanstack.
1. Install @mirafive/sdk-tanstack @mirafive/sdk-react @mirafive/sdk-browser @mirafive/sdk-server with
the project's package manager.
2. Add to .env (and the deployment's env): VITE_MIRAFIVE_KEY=<website key, mf_…> and
MIRAFIVE_SECRET_KEY=<secret key>. The secret key is server-only: never VITE_-prefixed, never
read in a component; only "@mirafive/sdk-tanstack/start" uses it.
3. In src/start.ts: const mirafive = miraMiddleware() at module scope (miraMiddleware from
"@mirafive/sdk-tanstack/start"), then createStart(() => ({ requestMiddleware: [mirafive] }))
(create the file if missing; keep existing middleware).
In src/routes/__root.tsx wrap the Outlet in
<MiraProvider websiteKey={import.meta.env.VITE_MIRAFIVE_KEY}> from "@mirafive/sdk-tanstack".
That alone counts pageviews on every navigation; do not add router subscriptions.
Track in components with useMira().track(name, props) or useTrackOnMount(name, props); on the
server inside createServerFn handlers with context.mira.track(name, { userId, properties }).
For flags: a server function returning (await context.flagsFor({ userId })).bootstrap(), called from
the root route's loader (staleTime: Infinity); pass bootstrap={bootstrap} plugins={[flags()]} to
MiraProvider (flags from "@mirafive/sdk-browser/flags"). The provider renders the flags block itself:
do not add <MiraFlagsScript> as well.
Read flags with useFlag(key, fallback) / useFlagConfig(key, fallback).
On Cloudflare Workers or Vercel pass the platform's waitUntil: miraMiddleware({ waitUntil }).
4. Keep the default consentless mode: it needs no banner. Only if a consent manager exists and ids
are wanted: plugins={[identity()]} from "@mirafive/sdk-browser/identity" plus mode="full", and
useMira().consent({ statistics, experiments, targeting }) in its callback.
5. Verify: run the production build (vite build); open a deployed page and check the network tab for
POST https://events.mirafive.io/v1/batch/<key> answering 202; server side, a server function running
await context.mira.send([{ name: "$install_check" }]) answers reason "install_check". Report what changed.
Do not add other analytics libraries, cookies or consent banners.Facts for agents:
- Imports (client):
import { MiraProvider, useMira, useFlag, useFlagConfig, useTrackOnMount } from "@mirafive/sdk-tanstack". Plugins:import { flags } from "@mirafive/sdk-browser/flags",/identity,/autocapture,/search,/experiments.pageviews()is added for you. - Imports (Start server side):
import { miraMiddleware, MiraFlagsScript } from "@mirafive/sdk-tanstack/start". The middleware is created once at module scope ofsrc/start.ts(const mirafive = miraMiddleware()), never inside thecreateStartfactory.MiraFlagsScriptis only for pages whose provider gets nobootstrap. - Env vars:
VITE_MIRAFIVE_KEY(public website key, passed aswebsiteKey),MIRAFIVE_SECRET_KEY(server only),MIRAFIVE_HOST(optional, server, defaulthttps://events.mirafive.io); the provider'shostprop sets the browser host. - Never ship
MIRAFIVE_SECRET_KEYto a browser bundle; a secret key in a browser is refused and marked exposed. Never prefix it withVITE_. - The provider prop is
websiteKey, notkey(React reserveskey), and it is required. - Consentless (default) needs no banner;
mode="full"needsidentity()and a consent answer, behind the site's CMP. - Nothing throws for transport reasons. Browser: dropped with a
[mirafive] …warning on local hosts only. Server:console.warn("[mirafive] …"), orsend()rejects withMiraError. - Verify an install:
vite buildpasses; a deployed page's network tab showsPOST …/v1/batch/{key}answering202;context.mira.send([{ name: "$install_check" }])answersreason: "install_check". - Wire contract: mirafive/protocol.
License
MIT © 2026 Cloo GmbH
