Back to docs

Getting started

Add FluxRun to your framework, then capture real executions.

Install the SDK, set the three server env vars, expose the agent route, then wrap the API path that should appear in FluxRun. The SDK already knows the production ingest endpoints, so normal apps do not set an ingest URL.

  1. Install the SDK

    shell
    npm install fluxrun
  2. Create token and keys

    Create an app in FluxRun, create a Flux project token, then create the public/private replay key pair for the app.

    .env
    FLUX_PROJECT_TOKEN=fbproj_...
    FLUX_PUBLIC_KEY=base64-public-key
    FLUX_PRIVATE_KEY=base64-private-key

    Keep FLUX_PRIVATE_KEY only on the server runtime that exposes traced routes and the agent route. Use FLUX_INGEST_URL only for private or self-hosted ingest overrides.

  3. Expose the shared agent route

    The dashboard calls this route for decrypt and replay with a short-lived token. The agent verifies that request with FluxRun by using FLUX_PROJECT_TOKEN.

    typescript
    // app/api/flux-agent/route.ts
    import { fluxAgent } from 'fluxrun';
    
    export const runtime = 'nodejs';
    
    const corsHeaders = {
      'Access-Control-Allow-Origin': 'https://app.fluxrun.dev',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
      'Access-Control-Allow-Methods': 'POST, OPTIONS',
    };
    
    export async function OPTIONS() {
      return new Response(null, { headers: corsHeaders });
    }
    
    export async function POST(req: Request) {
      const result = await fluxAgent(await req.json(), {
        authorization: req.headers.get('authorization'),
      });
      return Response.json(result, { headers: corsHeaders });
    }
  4. Choose your framework adapter

    Every adapter uses the same handler shape: return a status and body, read normalized request data from flux.request, and put live SDK clients or database handles behind fluxHost.

Next.js route handlers

Next.js App Router

fluxrun/adapters/next
next.config.tsapp/api/orders/route.tsapp/api/flux-agent/route.tsapp/api/flux-ingest/v1/executions/route.ts
  • Wrap next.config.ts with the Flux plugin so host-module calls are rewritten before Next compiles the route.
  • Use the Node.js runtime for traced routes and the agent route.
  • For local smoke tests, set FLUX_INGEST_URL=/api/flux-ingest; FluxRun posts to /api/flux-ingest/v1/executions.
typescript
// next.config.ts
import type { NextConfig } from 'next';
import { withFluxNextJsPlugin } from 'fluxrun/build';

const nextConfig: NextConfig = {};

const withFlux = withFluxNextJsPlugin();
export default withFlux(nextConfig);

// app/api/orders/route.ts
import { withFluxNextJs } from 'fluxrun/adapters/next';
import { fluxHost } from 'fluxrun';
import { prisma } from '@/lib/prisma';

export const runtime = 'nodejs';

type OrderInput = { amount: number; currency: string };

const db = fluxHost('db', {
  createOrder: async (input: OrderInput) => prisma.order.create({ data: input }),
});

export const POST = withFluxNextJs(
  'orders.create',
  async (flux) => {
    const body = flux.request.body as { amount: number; currency: string };
    const order = await db.createOrder(body);
    return { status: 201, body: { id: order.id } };
  },
  { host: { db } },
);

// app/api/flux-ingest/v1/executions/route.ts
type LocalFluxBatch = {
  executionId?: string;
  summary?: { executionId?: string };
  events?: unknown[];
};

export const runtime = 'nodejs';

export async function POST(req: Request) {
  const batch = (await req.json()) as LocalFluxBatch;
  return Response.json({
    ok: true,
    executionId: batch.executionId ?? batch.summary?.executionId ?? 'unknown',
    events: batch.events?.length ?? 0,
  });
}

Express middleware

Express

fluxrun/adapters/express
server.ts
  • Run express.json() before the Flux middleware when you need request bodies.
typescript
import express from 'express';
import { fluxAgent, fluxHost } from 'fluxrun';
import { withFluxExpress } from 'fluxrun/adapters/express';

const app = express();
app.use(express.json());

const db = fluxHost('db', {
  createOrder: (input: { amount: number }) => prisma.order.create({ data: input }),
});

app.post(
  '/api/orders',
  withFluxExpress(
    'orders.create',
    async (flux) => {
      const order = await flux.db.createOrder(flux.request.body as { amount: number });
      return { status: 201, body: { id: order.id } };
    },
    { host: { db } },
  ),
);

app.post('/api/flux-agent', async (req, res) => {
  res.json(await fluxAgent(req.body, { authorization: req.headers.authorization }));
});

Fastify route handler

Fastify

fluxrun/adapters/fastify
server.ts
  • Fastify parses JSON request bodies before the handler by default.
typescript
import Fastify from 'fastify';
import { fluxAgent, fluxHost } from 'fluxrun';
import { withFluxFastify } from 'fluxrun/adapters/fastify';

const app = Fastify();

const audit = fluxHost('audit', {
  write: (event: unknown) => auditClient.write(event),
});

app.post(
  '/api/orders',
  withFluxFastify(
    'orders.fastify.create',
    async (flux) => {
      await flux.audit.write({ path: flux.request.pathname, body: flux.request.body });
      return { status: 201, body: { ok: true } };
    },
    { host: { audit } },
  ),
);

app.post('/api/flux-agent', async (request, reply) => {
  const authorization = Array.isArray(request.headers.authorization)
    ? request.headers.authorization[0]
    : request.headers.authorization;
  return reply.send(await fluxAgent(request.body, { authorization }));
});

Hono handler

Hono

fluxrun/adapters/hono
src/index.ts
  • Use host modules for bindings and clients that cannot be serialized into the runtime.
typescript
import { Hono } from 'hono';
import { fluxAgent, fluxHost } from 'fluxrun';
import { withFluxHono } from 'fluxrun/adapters/hono';

const app = new Hono();

const billing = fluxHost('billing', {
  lookup: (id: string) => fetch('https://billing.example/' + id).then((r) => r.json()),
});

app.get(
  '/api/accounts',
  withFluxHono(
    'accounts.lookup',
    async (flux) => ({
      status: 200,
      body: await flux.billing.lookup(flux.request.searchParams['id'] ?? ''),
    }),
    { host: { billing } },
  ),
);

app.post('/api/flux-agent', async (c) =>
  c.json(
    await fluxAgent(await c.req.json(), {
      authorization: c.req.header('authorization') ?? null,
    }),
  ),
);

Koa middleware

Koa

fluxrun/adapters/koa
server.ts
  • Install and run koa-bodyparser before Flux when you need flux.request.body.
typescript
import Koa from 'koa';
import Router from '@koa/router';
import bodyParser from 'koa-bodyparser';
import { fluxAgent, fluxHost } from 'fluxrun';
import { withFluxKoa } from 'fluxrun/adapters/koa';

const app = new Koa();
const router = new Router();
app.use(bodyParser());

const mail = fluxHost('mail', {
  send: (to: string) => resend.emails.send({ to, subject: 'Hello' }),
});

router.post(
  '/api/invite',
  withFluxKoa(
    'invite.send',
    async (flux) => {
      const body = flux.request.body as { email: string };
      await flux.mail.send(body.email);
      return { status: 202, body: { queued: true } };
    },
    { host: { mail } },
  ),
);

router.post('/api/flux-agent', async (ctx) => {
  ctx.body = await fluxAgent(ctx.request.body, {
    authorization: ctx.get('authorization') || null,
  });
});

app.use(router.routes());

Node.js or Bun fetch handlers

Web Request runtimes

fluxrun/adapters/web
worker.tsserver.ts
  • Use this adapter when your runtime exposes standard Request and Response objects.
  • Do not advertise Cloudflare Workers support until the SDK has an edge-safe QuickJS build.
typescript
import { fluxAgent, fluxHost } from 'fluxrun';
import { withFluxWeb } from 'fluxrun/adapters/web';

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    if (new URL(request.url).pathname === '/api/flux-agent') {
      return Response.json(
        await fluxAgent(await request.json(), {
          authorization: request.headers.get('authorization'),
        }),
      );
    }

    const queue = fluxHost('queue', {
      send: (message: unknown) => env.ORDERS.send(message),
    });

    const orders = withFluxWeb(
      'worker.orders',
      async (flux) => {
        await flux.queue.send({ token: flux.env.API_TOKEN, body: flux.request.body });
        return { status: 202, body: { queued: true } };
      },
      { host: { queue } },
    );

    return orders(request, env, ctx);
  },
};

API Gateway HTTP API v2 or Lambda Function URL

AWS Lambda

fluxrun/adapters/aws
handler.ts
  • Expose the agent as a separate Lambda route or function URL using the same env vars.
typescript
import { fluxAgent, fluxHost } from 'fluxrun';
import { withFluxLambda } from 'fluxrun/adapters/aws';

const crm = fluxHost('crm', {
  findAccount: (region: string) => crmClient.accounts.find({ region }),
});

export const handler = withFluxLambda(
  'lambda.accounts',
  async (flux) => {
    const region = flux.request.searchParams['region'] ?? 'us';
    return { status: 200, body: await flux.crm.findAccount(region) };
  },
  { host: { crm } },
);

export const agent = withFluxLambda('lambda.agent', async (flux) => ({
  status: 200,
  body: await fluxAgent(flux.request.body),
}));

Advanced client adapters

Postgres, Redis, and MongoDB

These are drop-in client adapters for specific data calls. Prefer a fluxHost module for normal app code because it makes the boundary explicit in executions and replay.

PostgreSQL

fluxrun/adapters/pg
typescript
import { Pool } from 'fluxrun/adapters/pg';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const { rows } = await pool.query('select * from users where id = $1', [id]);

Redis

fluxrun/adapters/redis
typescript
import Redis from 'fluxrun/adapters/redis';

const redis = new Redis(process.env.REDIS_URL);
await redis.set('order:last', order.id);

MongoDB

fluxrun/adapters/mongo
typescript
import { MongoClient, ObjectId } from 'fluxrun/adapters/mongo';

const client = new MongoClient(process.env.MONGO_URL);
const user = await client.db('app').collection('users').findOne({ _id: new ObjectId(id) });

Server actions & native mode

Next.js server actions, webhooks, and fluxTrack

Use withFluxNextServerAction from fluxrun/adapters/next-server-action for mutations via form actions. Runs in native mode so redirect(), revalidatePath(), and cookies() work normally. Works with both plain actions and useActionState.

typescript
import { withFluxNextServerAction } from 'fluxrun/adapters/next-server-action';

export const createOrder = withFluxNextServerAction(
  'orders.create',
  async (formData: FormData) => {
    const amount = formData.get('amount');
    await db.createOrder({ amount: Number(amount), currency: 'usd' });
    redirect('/orders');
  },
  { host: { db } },
);

For webhooks that need raw body access (HMAC verification), use { rawBody: true }. For middleware or auth guards that need the original Request object, use { passRequest: true, mode: 'native' } and access it via flux.request.raw.

typescript
import { withFluxNextJs, fluxTrack } from 'fluxrun';

// Webhook with HMAC verification
export const POST = withFluxNextJs(
  'webhooks.github',
  async (flux) => {
    const sig = flux.request.headers['x-hub-signature-256'];
    verifyHmac(sig, flux.request.rawBody!);
    // ...
  },
  { rawBody: true },
);

// Native recording for middleware/auth
export const GET = withFluxNextJs(
  'api.protected',
  async (flux) => {
    const req = flux.request.raw; // original Request
    const ip = req.headers.get('x-forwarded-for');
    // ... auth logic with fluxTrack ...
  },
  { passRequest: true, mode: 'native' },
);

Manual fallback

Use fluxFunc when no framework adapter fits

Manually wrap the function you want to trace, call it from your existing handler, and expose the same agent route. This is the path for custom servers, job runners, cron workers, and frameworks without a dedicated adapter.

typescript
import { fluxFunc, fluxHost, fluxAgent } from 'fluxrun';

const payments = fluxHost('payments', {
  charge: (order: Order) => paymentClient.charge(order),
});

const createOrder = fluxFunc(
  async (order: Order) => {
    const charge = await payments.charge(order);
    return { ok: true, chargeId: charge.id };
  },
  'orders.create',
  { host: { payments } },
);

export async function handleOrder(req: Request) {
  const result = await createOrder(await req.json());
  return Response.json(result, { status: 201 });
}

export async function handleFluxAgent(req: Request) {
  return Response.json(
    await fluxAgent(await req.json(), {
      authorization: req.headers.get('authorization'),
    }),
  );
}

Verify and trigger

Add await fluxCheck() at startup to verify project token, public key, and ingest connectivity. Then save the deployed agent URL in FluxRun, call the wrapped API path like a normal user, then open app.fluxrun.dev . A successful setup shows a new execution under the app with captured request, return, console, errors, and replay actions.