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.

Client / curl
    ↓
[ 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

API Gateway

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.

Runtime

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.

Data Engine

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.

Queue

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.

API + Telemetry

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:

  1. Client sends POST /create_order with a project API key.
  2. Gateway authenticates the key, checks rate limits, stamps x-request-id: 9c3e7f1a, routes to create_order handler.
  3. Runtime initialises ctx with traceId = "9c3e7f1a", calls your handler.
  4. Your handler calls ctx.db.query({ table: "orders", operation: "insert", ... }).
  5. Data Engine executes the SQL, records a span: 12 ms, insert, orders, returns rows.
  6. Handler returns { orderId: "..." }, runtime serialises it, gateway responds.
  7. All spans (gateway → runtime → db) are stored under 9c3e7f1a.
  8. flux trace 9c3e7f1a assembles and renders the full trace.

Async request lifecycle

When your handler triggers an async function (e.g. send_invoice with "async": true):

  1. The runtime enqueues the invocation with the current x-request-id attached.
  2. Your handler returns immediately — the HTTP response goes back to the client.
  3. The queue dequeues and dispatches send_invoice with the inherited trace ID.
  4. The function executes exactly like any synchronous function — with its own spans.
  5. Its spans are associated with the original x-request-id, so they appear in the same trace under an async → branch.

Key design principles

PrincipleWhat it means
One route per functionEach function maps to exactly one HTTP route. The gateway enforces this — no ambiguous routing.
Trace ID is the threadThe x-request-id is the single thread that connects all layers. It travels through every service call.
Structured queries onlyThe 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 SDKYou never import an observability SDK. Tracing, logging, and span collection happen in the platform layers, not in your function code.
Async is first-classBackground 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.

ServiceExposed toLanguage
API GatewayPublic — your users call itRust (Axum)
RuntimeInternal — gateway routes to itRust + Deno
Data EngineInternal — called by Runtime via ctx.dbRust
QueueInternal — called by Runtime for asyncRust
APICLI + DashboardRust (Axum)

← Observability Next: API Gateway →