fluxrun
v0.0.22
Published
fluxRun wraps async functions so every execution is recorded, encrypted, and replayable — without changing how you write or call your code.
Readme
fluxrun
fluxRun wraps async functions so every execution is recorded, encrypted, and replayable — without changing how you write or call your code.
How it works
your async function
└─ wrapped with fluxFunc(fn, "name")
├─ runs inside an isolated QuickJS sandbox
├─ every side effect (fetch, env, timers, …) crosses a Flux boundary
├─ boundary events are encrypted and sent to the FluxBase backend
└─ replay re-runs the function using the recorded events — no real IO firesQuick start
import { fluxFunc, fluxENV } from 'fluxrun';
export const createOrder = fluxFunc(async (order: Order) => {
const apiKey = fluxENV.PAYMENT_API_KEY;
const response = await fetch('https://payments.example.com/charge', {
method: 'POST',
headers: { authorization: `Bearer ${apiKey}` },
body: JSON.stringify(order),
});
return response.json();
}, 'orders.create');
// Call it like a normal function — Flux handles tracing automatically.
const result = await createOrder({ amount: 99_00, currency: 'usd' });End-to-end flow
1. Developer wraps functions with fluxFunc(...)
2. Each execution is recorded and encrypted at the boundary
3. Encrypted trace is sent to the FluxBase backend (POST /v1/executions)
4. Developer runs an agent endpoint on their own server:
import { fluxAgent } from "fluxrun";
// Express / Next.js / Bun.serve / any HTTP framework:
app.post("/api/flux-agent", async (req, res) => {
const result = await fluxAgent(req.body, {
authorization: req.headers.authorization,
});
res.json(result);
});
5. Developer registers their agent URL in the FluxBase dashboard
6. Dashboard can now:
- Decrypt any captured payload (private key never leaves the developer's server)
- Trigger replay — re-runs the function deterministically, no real IO firesWhat fluxRun records
| Capability | What is captured |
| --------------- | ------------------------------------------------ |
| fetch | URL (path only), method, headers, body, response |
| fluxENV | Key names and values (fully encrypted) |
| console | All log methods and arguments |
| Math.random() | Generated values |
| crypto | randomUUID(), getRandomValues(), digest() |
| Date.now() | Timestamps |
| timers | setTimeout / setInterval scheduling + fire |
| fluxHost RPC | Module name, method, arguments, return value |
| fluxFetch | URL, method, headers, status, response |
| vm | Function entry args + final result or error |
Encryption
Sensitive captured payloads are encrypted before leaving the process using hybrid RSA-OAEP + AES-256-GCM encryption (Web Crypto). The FluxBase backend stores ciphertext for payload data it does not need to route or index.
Sensitive fields (env values, fetch bodies, auth headers, URL query params)
and console arguments are encrypted. Non-sensitive routing fields (method,
status code, URL path, timer IDs, etc.) stay plaintext so the dashboard remains
readable without requiring decryption for every inspection.
Generate a key pair from the FluxRun setup screen. It emits portable, single-line values:
# Public key for app servers
FLUX_PUBLIC_KEY=fluxpub_v1_...
# Private key for the replay agent only
FLUX_PRIVATE_KEY=fluxpriv_v1_...PEM keys are still accepted for existing installs, but the compact values are
easier to copy into .env, hosting dashboards, and secret managers.
Replay guarantees
During replay fluxRun:
- Does not call any external systems.
- Serves recorded boundary responses back to the sandbox in the original order.
- Produces the same final result for the same trace.
- Is safe to run in production environments — no side effects.
Authoring primitives
fluxFunc(fn, name, options?)
Wraps a function so it executes in the fluxRun sandbox and records a full trace. The function signature is preserved — callers do not need to change anything.
fluxTrack(fn, name, options?)
Native recording alternative — same lifecycle events and fluxHost RPC recording,
but executes directly in the host process without QuickJS sandboxing.
import { fluxTrack } from 'fluxrun';
// Use for server actions, middleware, or code that needs
// redirect(), cookies(), or any Node.js API QuickJS can't access.
export const charge = fluxTrack(async (amount: number) => {
const res = await fluxFetch('https://api.stripe.com/v1/charges', {
method: 'POST',
headers: { Authorization: `Bearer ${fluxENV.STRIPE_KEY}` },
body: JSON.stringify({ amount, currency: 'usd' }),
});
return res.json();
}, 'billing.charge');Equivalent to fluxFunc(fn, name, { mode: 'native' }).
export const getUser = fluxFunc(async (id: string) => {
return db.users.findUnique({ where: { id } });
}, 'user.get');For per-request trace labels, keep the function arguments unchanged and call it
through withContext:
const createOrder = fluxFunc(async (order: Order) => order.id, 'orders.create');
await createOrder.withContext({ requestId: 'req_123' })({ id: 1 });fluxHost(name, impl)
Registers a host-side object so its methods are callable from inside a
fluxFunc sandbox. Every call crosses the boundary and is recorded/replayed.
// Register once at startup:
const db = fluxHost('db', {
query: (sql: string, params: unknown[]) => pool.query(sql, params),
});
// Then use naturally inside fluxFunc:
export const listUsers = fluxFunc(async () => {
return db.query('SELECT * FROM users', []);
}, 'users.list');Use fluxHost for live resources: database clients, SDK instances, queues,
filesystem access, or anything with mutable host state.
For large store objects with many methods, use fluxHost.auto() to auto-discover
every function property — no manual fluxHost call per method needed:
// With 54 methods, this is one line instead of 54:
const db = fluxHost.auto('db', {
user: { findMany: ..., findUnique: ..., create: ..., update: ..., delete: ... },
order: { findMany: ..., create: ..., ... },
// ... all 54 methods auto-registered
});fluxHost.auto iterates Object.keys(impl), registers every function property
into the host registry, and returns a traced Proxy with full async type
preservation.
fluxInline(fn)
Marks a pure helper function so it is copied into the QuickJS runtime instead
of called through RPC. This keeps helper logic inside the recorded execution,
while any capabilities used by the helper (fetch, Math.random, fluxENV,
timers, etc.) are still captured normally.
import { fluxFunc, fluxInline } from 'fluxrun';
const normalizeEmail = fluxInline(function normalizeEmail(email: string) {
return email.trim().toLowerCase();
});
export const createUser = fluxFunc(async (email: string) => normalizeEmail(email), 'users.create', {
bindings: { normalizeEmail },
});Only use fluxInline for deterministic helpers that do not close over live
objects. Plain function bindings still use RPC by default for safety.
fluxENV
Safe environment access that works both in host code and inside the QuickJS sandbox. Reads are recorded and replayed like any other boundary call.
const apiKey = fluxENV.STRIPE_SECRET_KEY; // works inside or outside fluxFuncfluxFetch(url, init?)
Host-side fetch wrapper that auto-injects x-request-id and W3C traceparent
headers from the execution context. Every call is recorded as a fetch boundary
event, visible in the FluxRun dashboard alongside sandboxed fetch calls.
import { fluxFetch } from 'fluxrun';
export const charge = fluxFunc(async (amount: number) => {
const res = await fluxFetch('https://api.stripe.com/v1/charges', {
method: 'POST',
headers: { Authorization: `Bearer ${fluxENV.STRIPE_KEY}` },
body: new URLSearchParams({ amount: String(amount), currency: 'usd' }),
});
return res.json();
}, 'billing.charge');When called outside an execution context, fluxFetch falls back to
globalThis.fetch with no tracing or recording.
Postgres / SQL tracing
FluxRun ships with a drop-in pg adapter that routes queries through the
Flux boundary layer so every SQL call is recorded and replayable.
import { Pool } from 'fluxrun/adapters/pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const getUser = fluxTrack(async (id: string) => {
const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
return rows[0];
}, 'db.users.get');For local development or when you already have a pg.Pool instance, use
fluxHost.auto() instead — it wraps every method on the object with
boundary recording without changing imports:
import { Pool } from 'pg';
import { fluxHost, fluxTrack } from 'fluxrun';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
fluxHost.auto('db', pool);
export const getUser = fluxTrack(async (id: string) => {
const { rows } = await (globalThis as any).db.query('SELECT * FROM users WHERE id = $1', [id]);
return rows[0];
}, 'db.users.get');fluxAgent(payload, options?)
The agent endpoint handler. Pass parsed request body → get back a JSON-safe result. Handles decryption and replay.
import { fluxAgent } from 'fluxrun';
export async function POST(req: Request) {
const result = await fluxAgent(await req.json(), {
authorization: req.headers.get('authorization'),
});
return Response.json(result);
}Framework adapters
Each adapter exports withFlux*(name, handler, options?). The handler always
receives the same flux shape (request, context, env, plus fluxHost
modules). Install the framework you use as a normal app dependency; fluxrun
lists matching versions as optional peerDependencies for discoverability.
Next.js in 5 minutes
Use the adapter in App Router route handlers, and wrap your next.config.ts
with the Flux plugin. The plugin installs both webpack and Turbopack loader
rules so host-module calls can be rewritten before Next compiles the route.
next.config.ts
import type { NextConfig } from 'next';
import { withFluxNextJsPlugin } from 'fluxrun/build';
const nextConfig: NextConfig = {};
const withFlux = withFluxNextJsPlugin();
export default withFlux(nextConfig);lib/flux-bindings.ts
import { fluxHost } from 'fluxrun';
import { prisma } from '@/lib/prisma';
type OrderInput = { amount: number; currency: string };
export const db = fluxHost('db', {
createOrder: async (input: OrderInput) => prisma.order.create({ data: input }),
});app/api/orders/route.ts
import { withFluxNextJs } from 'fluxrun/adapters/next';
import { db } from '@/lib/flux-bindings';
export const runtime = 'nodejs';
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-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 });
}Set FLUX_PROJECT_TOKEN, FLUX_PUBLIC_KEY, and FLUX_PRIVATE_KEY on the
server runtime that exposes the traced routes and the agent route. FluxBase
calls the agent with a short-lived dashboard token; fluxAgent verifies that
token with FluxBase by using FLUX_PROJECT_TOKEN.
The adapter automatically populates flux.requestId and flux.traceId from
incoming headers (x-request-id, traceparent) for cross-service tracing.
For webhook handlers that need the raw body (HMAC verification), pass rawBody:
true:
export const POST = withFluxNextJs(
'webhooks.github',
async (flux) => {
const sig = flux.request.headers['x-hub-signature-256'];
verifyHmac(sig, flux.request.rawBody!);
// ...
},
{ rawBody: true },
);Adapter First Run
Every adapter follows the same production checklist:
- Set
FLUX_PROJECT_TOKEN,FLUX_PUBLIC_KEY, andFLUX_PRIVATE_KEYwhere your app runs. - Put live SDK clients, database handles, queues, and non-serializable objects
behind
fluxHost. - Make sure your framework body parser runs before Flux when the framework does not parse bodies automatically.
The SDK ships with production ingest failover URLs. Use FLUX_INGEST_URL or
FLUX_INGEST_URLS only for private/self-hosted ingest overrides.
Local ingest proxy (Next.js)
For local development you can route ingest through your own app so CORS or network policies don't block the SDK. Set:
FLUX_INGEST_URL=/api/flux-ingestFluxRun appends /v1/executions to that base URL, so the SDK posts capture
batches to /api/flux-ingest/v1/executions.
Important: that local route must forward batches to the real ingest endpoint or you will see 0 executions in the dashboard. Use the built-in proxy helper:
app/api/flux-ingest/v1/executions/route.ts
import { createFluxIngestProxy } from 'fluxrun/adapters/next';
export const runtime = 'nodejs';
export const POST = createFluxIngestProxy();By default it proxies to https://ingest-0.fluxrun.dev/v1/executions and
forwards your FLUX_PROJECT_TOKEN automatically. You can override the
destination if you self-host ingest:
export const POST = createFluxIngestProxy('https://my-ingest.example.com/v1/executions');Local dry-run mode
Set FLUX_DRY_RUN=1 to skip the public key requirement and log capture
batches as JSON to stdout instead of POSTing to the ingest service:
FLUX_DRY_RUN=1 node server.jsThis works without any credentials — useful for integration testing, CI smoke tests, and verifying recording setup before deploying.
Debug mode
Set FLUX_DEBUG=1 to print every capture event, batch flush, and delivery
attempt to the console. This is the fastest way to diagnose why executions
aren't appearing:
FLUX_DEBUG=1 node server.jsDoctor
Run fluxrun doctor from your app to verify the SDK pipeline before opening
the dashboard. It checks the installed SDK version, required environment
variables, ingest health, token delivery, and the optional replay agent route:
FLUX_AGENT_URL=https://your-app.com/api/flux-agent fluxrun doctorIf you set FLUX_INGEST_URL=/api/flux-ingest, doctor warns when it sees a
relative local route because dashboard delivery depends on that route forwarding
with createFluxIngestProxy.
Console capture
Set FLUX_CAPTURE_HOST_CONSOLE=1 to record console.log, console.error,
console.warn, and console.info calls that happen in adapter boilerplate
(outside the QuickJS sandbox) as boundary events visible in the dashboard:
Express
Use express.json() before the Flux middleware. Register the agent endpoint in
the same app or in a separate private service.
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 }));
});Hono
Hono gives the adapter a web Request; use host modules for bindings that
cannot be serialized.
import { Hono } from 'hono';
import { fluxAgent, fluxHost } from 'fluxrun';
import { withFluxHono } from 'fluxrun/adapters/hono';
type Bindings = {
API_TOKEN: string;
};
const app = new Hono<{ Bindings: Bindings }>();
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,
}),
),
);Fastify
Fastify parses JSON bodies for you. flux.request.pathname is the request path
without the query string, and flux.request.searchParams contains the query.
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 }));
});Koa
Install a body parser such as koa-bodyparser before Flux when you need
flux.request.body.
import Koa from 'koa';
import bodyParser from 'koa-bodyparser';
import Router from '@koa/router';
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());Web Request runtimes
For Node.js or Bun runtimes that expose standard Web Request / Response
objects, export fetch(request, env, ctx). String environment bindings are
copied into flux.env; live objects should be exposed through fluxHost.
Cloudflare Workers are not part of the current publish contract because the Flux runtime depends on the Node.js QuickJS bootstrap. Do not advertise Workers support until the runtime has a verified edge-safe QuickJS build.
import { fluxAgent, fluxHost } from 'fluxrun';
import { withFluxWeb } from 'fluxrun/adapters/web';
type Env = {
API_TOKEN: string;
ORDERS: { send(message: unknown): Promise<void> };
};
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);
},
};AWS Lambda
API Gateway HTTP API v2 and Function URLs provide rawQueryString; Flux parses
it into flux.request.searchParams when queryStringParameters is absent.
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),
}));TypeScript utilities
fluxrun exports helper types for common patterns:
import type { ExtractParams, FluxResult, RequireDefined } from 'fluxrun';
// Next.js App Router route params
type RouteCtx = { params: Promise<{ id: string }> };
type P = ExtractParams<RouteCtx>; // { id: string }
// Discriminated result wrapper — no `| undefined` issues
async function getOrder(id: string): FluxResult<{ total: number }> {
const o = await db.find(id);
if (!o) return { ok: false, error: 'Not found' };
return { ok: true, data: o };
}
// Strip `| null | undefined` from GraphQL types
type User = { name?: string | null; profile?: { bio?: string | null } | null };
type Clean = RequireDefined<User>; // { name: string; profile: { bio: string } }Closures and bindings
QuickJS evaluates serialized function source so closures do not work out of the box.
- Pass data as arguments — cleanest approach for plain values.
- Use
bindings— inject serializable values at call time:
const config = { apiUrl: 'https://api.example.com' };
const fetchUser = fluxFunc(
async (id: string) => fetch(`${config.apiUrl}/users/${id}`),
'user.fetch',
{ bindings: { config } },
);- Use
fluxInline— explicitly copy pure helper functions into the runtime:
const score = fluxInline((value: number) => value * 2);
const rank = fluxFunc(async (value: number) => score(value), 'score.rank', {
bindings: { score },
});- Use
fluxHost— for live objects, class instances, SDK clients, and anything that cannot be serialized.
Package exports
| Export | Description |
| -------------------------- | ----------------------------------------------------- |
| fluxrun | Main package (fluxFunc, fluxHost, fluxFetch, …) |
| fluxrun/agent | Agent handler (fluxAgent) |
| fluxrun/build | esbuild plugins for build-time transforms |
| fluxrun/adapters/pg | PostgreSQL HTTP adapter for edge targets |
| fluxrun/adapters/redis | Redis HTTP adapter for edge targets |
| fluxrun/adapters/mongo | MongoDB HTTP adapter for edge targets |
| fluxrun/adapters/next | Next.js App Router Request → Response |
| fluxrun/adapters/express | Express middleware |
| fluxrun/adapters/hono | Hono handler → Response |
| fluxrun/adapters/fastify | Fastify route handler |
| fluxrun/adapters/koa | Koa middleware |
| fluxrun/adapters/web | Web Request → Response |
| fluxrun/adapters/aws | Lambda handler (API Gateway v2 style) |
Development
bun install
bun test # run the test suite
bun run build # compile to dist/