Client-side TypeScript framework · Lit · MobX 7

A static core.
Flux everywhere else.

StaticFlux is a small, stable core of contracts — tokens, units, scopes, results, state machines, telemetry — and an ecosystem of building blocks that plug into it. Each package works on its own. Together they make large single-page apps fast, observable and boring to change.

Core boot surface
≤ 7.5 KB
Packages
34 + 4 tools
Boot JS budget
≤ 150 KB
A static core with flux around it A solid core in the centre; around it, orbits carrying the pluggable implementations. router auth gate worker data ui core ≤ 7.5 KB

Declare, don’t wire

Every piece says what it needs. The injector does the rest.

A unit declares requires and provides with typed tokens. Boot resolves critical units in dependency order and traces each one into a boot waterfall; everything else stays lazy until something asks for it. A missing provider is a type error at the composition site, not a 2 a.m. page.

Core & DI guide

app/compose.ts
import { type Result, token, unit } from '@staticflux/core';
import { bootApp } from '@staticflux/runtime';
import { DataClientToken, dataClientUnit, epochSourceUnit, mockTransportUnit, pauseControlUnit } from '@staticflux/data';
import { DocsList, type DocsPage } from './endpoints.js';

export interface DocsApi { list(signal: AbortSignal): Promise<Result<DocsPage>> }
export const DocsApiToken = token('docs/api').type<DocsApi>();

export const docsApiUnit = unit({
  id: 'docs/api-unit',
  provides: DocsApiToken,
  scope: 'app',
  deps: { required: { client: DataClientToken } },
  create: ({ client }): DocsApi => ({
    list: (signal) => client.call(DocsList, { cursor: undefined }, { signal }),
  }),
});

const booted = await bootApp({
  units: [
    docsApiUnit, dataClientUnit, epochSourceUnit, pauseControlUnit,
    mockTransportUnit((mock) => mock.route(DocsList, () => ({ items: [], nextCursor: null }))),
  ],
});

The fabric

Five ideas, applied to everything.

The same five ideas shape the router, the session, the surface manager and your features alike. Learn them once; every package reads the same way.

  1. 01

    Declarations

    Tokens, flags, metrics, routes, capabilities and errors are typed values — never magic strings. Misconfiguration is a compile-time or CI error.

    token('docs/api').type<DocsApi>()
  2. 02

    A requires / provides graph

    Every unit declares what it needs and what it offers. The injector resolves in topological order, times every resolution and refuses cycles and scope leaks.

    deps: { required, lazy, functional }
  3. 03

    Scopes & lifecycles

    Every lifecycle is a typed state machine; every resource lives in a disposal scope with an AbortSignal. Leaving a state cleans up — always.

    scope.listen(window, "resize", fit)
  4. 04

    Plugins, not branches

    Experiments, modes and hosts are alternate bindings and separate features. Feature code never asks “which variant am I?”

    binder.variants({ flag, of, variants })
  5. 05

    One telemetry fabric

    DI, navigation stages, chunks, requests, gate decisions, workers and renders all emit OpenTelemetry-shaped spans. Devtools are just a consumer.

    metric.histogram('docs/render', { unit: 'ms' })

Building blocks

Verticals that work alone — and best together.

Opting in is one line and immediately pays off: register perfUnits and you get journey traces; register gateUnits and every route, slot and capability can be gated with explanations.

All 38 packages and tools

One stop for the modern web

Use the newest platform APIs. Ship to every browser.

StaticFlux packages feature-detect every modern API and degrade deliberately — Safari and Firefox gaps included. You write the feature once; the platform facts pick the path. Experimental APIs live in adapter packages, so the core never churns when a spec does.

  • Client-only: no SSR server to run, backends behind typed contracts
  • Everything lazy by default, eager only with a declared reason
  • Budgets for bytes, boot time, feature marks — enforced in CI
  • Accessible, localizable and testable by default
  • Navigation APIHistory API fallback; memory history for embedded hosts
  • scheduler.yield / postTaskMessageChannel + setTimeout shim (Safari)
  • View Transitionsfor route, view and workspace changes; skipped on reduced motion
  • Speculation Rulesprogressive enhancement over modulepreload and SW warmup
  • SharedWorker + Web Locksmulti-tab leader election; BroadcastChannel fallback
  • OffscreenCanvas · WebGPUrender workers; main-thread fallback with the same engine
  • HTML-in-Canvasfeature-detected adapter; positioned DOM overlay fallback
  • Wasm SIMD · threadsvariant builds picked from feature facts and isolation
  • COOP / COEP isolationper-app choice; shared vs transfer channels picked for you
  • Built-in AIone model-provider port; availability is a reactive fact
  • WebMCPcapabilities published as agent tools, gated as subject “agent”
  • IndexedDB · OPFSone async AsyncStore; localStorage and memory fallbacks
  • SharedArrayBuffer + Atomicspipes between workers when isolated; ordered postMessage mirrors otherwise
  • EditContexttext input port; hidden-textarea fallback chosen by a variant binding

Agent-first codebase

Explicit enough for humans. Explicit enough for agents.

Every package ships an AGENTS.md with its purpose, public API by file, rules and budgets. Every error has a stable code and a fix, collected in a machine-readable catalog. Boundaries, budgets and lint rules are enforced by tooling — so nobody, human or AI, has to guess.

Read the philosophy

docs/errors.json
{
  "code": "SF-DI-002",
  "kind": "invariant",
  "summary": "A longer-lived unit requires a shorter-lived unit.",
  "fix": "Move the dependency to a lazy dep, or lower the consumer scope."
}

Start with one unit. Grow into the whole fabric.

Clone the monorepo, run the checks, boot the Studio example — an AI-assisted document editor composing every vertical against mock services.