Architecture
How Flux works: five layers that turn a TypeScript function into a fully observable, production-ready API endpoint.
Overview
Every request to a Flux project travels through a fixed set of layers. Each layer has a single responsibility, and every layer emits structured telemetry. The result is complete observability with zero configuration from your side.
↓
[ API Gateway ] auth · rate limiting · CORS · routing
↓ x-request-id propagated
[ Runtime ] Deno function execution · ctx injection
↓ ↘ (async)
[ Data Engine ] structured queries · trace spans · N+1 detection
↘ (fire-and-forget)
[ Queue ] async function dispatch · retry · ordering
↓ all spans aggregated
[ API / Observability ] traces · logs · schema graph · suggestions
The five layers
Handles every request before your code runs
The gateway enforces authentication (project API keys + JWT), validates rate limits, applies CORS headers, and routes the request to the correct function based on the route and HTTP method defined in flux.json. It also stamps every request with an x-request-id used for distributed tracing.
Executes your TypeScript function
The runtime is a Deno-based execution environment. It receives the routed request, deserialises input, injects the FluxContext (ctx), and calls your handler. ctx.db, ctx.secrets, and ctx.log all route through the runtime, which attaches the trace ID to every downstream call.
Executes structured database queries with observability
The Data Engine is a Rust service that translates the structured query format from ctx.db.query() into SQL, executes it against your project's Postgres database, and emits a trace span including table, operation, duration, and cache status. It detects N+1 patterns and generates index suggestions in real time.
Runs async functions as background flow steps
When a function has "async": true in its flux.json, invocations go through the queue. The queue handles dispatch, retry on failure, ordering, and propagates the original x-request-id so async steps appear in the same trace as the originating synchronous request.
Aggregates all telemetry and surfaces it via CLI and Dashboard
The main API collects spans from the gateway, runtime, data engine, and queue. It assembles them into complete request traces, detects patterns (N+1, slow queries), and provides the data powering flux trace, flux logs, and the dashboard. The schema graph (/schema/graph) shows your project's full table structure with column types and suggested indexes.
Request lifecycle
Here is what happens on every synchronous API call:
- Client sends
POST /create_orderwith a project API key. - Gateway authenticates the key, checks rate limits, stamps
x-request-id: 9c3e7f1a, routes tocreate_orderhandler. - Runtime initialises
ctxwithtraceId = "9c3e7f1a", calls your handler. - Your handler calls
ctx.db.query({ table: "orders", operation: "insert", ... }). - Data Engine executes the SQL, records a span:
12 ms, insert, orders, returns rows. - Handler returns
{ orderId: "..." }, runtime serialises it, gateway responds. - All spans (gateway → runtime → db) are stored under
9c3e7f1a. flux trace 9c3e7f1aassembles and renders the full trace.
Async request lifecycle
When your handler triggers an async function (e.g. send_invoice with "async": true):
- The runtime enqueues the invocation with the current
x-request-idattached. - Your handler returns immediately — the HTTP response goes back to the client.
- The queue dequeues and dispatches
send_invoicewith the inherited trace ID. - The function executes exactly like any synchronous function — with its own spans.
- Its spans are associated with the original
x-request-id, so they appear in the same trace under anasync →branch.
Key design principles
| Principle | What it means |
|---|---|
| One route per function | Each function maps to exactly one HTTP route. The gateway enforces this — no ambiguous routing. |
| Trace ID is the thread | The x-request-id is the single thread that connects all layers. It travels through every service call. |
| Structured queries only | The ctx.db API accepts a structured query object — never raw SQL. This enables the data engine to emit rich spans and detect query patterns. |
| No user-side SDK | You never import an observability SDK. Tracing, logging, and span collection happen in the platform layers, not in your function code. |
| Async is first-class | Background work is not an afterthought. The queue is a first-class layer with its own spans, retry logic, and full trace visibility. |
Service URLs
All services are internal to the Flux platform. From your function, you interact through ctx. The CLI talks to the public-facing API. You never configure service URLs.
| Service | Exposed to | Language |
|---|---|---|
| API Gateway | Public — your users call it | Rust (Axum) |
| Runtime | Internal — gateway routes to it | Rust + Deno |
| Data Engine | Internal — called by Runtime via ctx.db | Rust |
| Queue | Internal — called by Runtime for async | Rust |
| API | CLI + Dashboard | Rust (Axum) |