Your Frontend App
Has a Runtime

Frontend apps accumulate runtime contracts — readiness, dependencies, cleanup — inside provider trees and convention. Frond names the graph.

Every frontend app has a runtime. It may not have a runtime package, runtime docs, or a file named runtime.ts, but it has runtime behavior: identity, dependencies, readiness, cancellation, cleanup, and error propagation.

React renders the app. It does not own those contracts for you.

So the contracts leak into places that were never designed to carry them. Provider order becomes dependency order. Boolean guards become readiness rules. Logout becomes a cleanup graph. Error boundaries get asked to explain failures that started three async edges away.

You can see the runtime in the code nobody wants to touch.

async function signOut() {
  await auth.signOut();

  localStorage.removeItem("token");
  queryClient.clear();
  socket.disconnect();
  analytics.reset();
  abortProfileRequests();
  billingStore.reset();
  flagsStore.reset();
  navigate("/login");
}

That function is a dependency graph written as a checklist. Most lines touch a user-scoped resource. The order is the graph. The missing abstraction is not state. The missing abstraction is ownership.

React gives you a tree

React gives you a component tree. Frontend runtime contracts often need a dependency graph.

Most apps bridge that mismatch with provider order, render guards, cache keys, registries, and cleanup scripts. Each piece is locally reasonable. Together they become architecture without an owner.

The graph you already have

Map out any screen in your app:

  AuthService ← SessionStore ← ApiClient    ← services (init order matters)
       ↓              ↓            ↓
  FeatureFlags    UserProfile   SocketTransport  ← resources (need services ready)
       ↓              ↓            ↓
       └──────→ DashboardScreen ←──┘         ← screen (needs all of the above)

Identity. Dependencies. Readiness order. Cleanup cascades. This is a graph. It’s just never drawn as one.

What counts as runtime

The frontend runtime is the layer that answers operational questions before a component tries to render:

  • which thing has identity?
  • which thing depends on which other thing?
  • when is a thing ready?
  • what should cancel when the root changes?
  • what should release when it leaves?
  • how should an error explain its cause chain?

State management answers a narrower question: where does the value live?

That question matters. It does not cover the lifecycle around the value. A query cache can hold profile data. A store can expose a domain object. A context provider can pass down an API client. None of those choices, by themselves, tell the rest of the app what depends on the current session, what should abort during logout, or why a screen could not become ready.

Once you name the runtime, the scattered patterns become easier to classify. That render guard — enabled: Boolean(user && api.ready) — is a readiness gate. The logout checklist is a reverse-dependency cleanup. The vague Sentry error is a graph failure that lost its graph. Even provider order is just an undeclared dependency edge.

Frond names the graph

Frond is a frontend runtime for declaring that graph directly.

It can carry state. A node is MobX-backed, so it can behave like a store when that is the right shape. The point is narrower: Frond owns the lifecycle around the state.

The public split is plain: @frondruntime/core owns the graph; @frondruntime/react gives React a provider and hooks. The runtime has nodes for resources, services, and facades. Nodes declare identity, dependencies, acquisition, actions, refresh, release, and cleanup in one place.

As a sketch, a profile resource becomes a node with declared dependencies and one acquisition boundary.

import * as Frond from "@frondruntime/core";

type ProfileSpec = Frond.NodeSpec<{
  args: Frond.Args.None;
  key: Frond.Key.Singleton;
  result: Profile;
}>;

export class ProfileNode extends Frond.NodeBase<ProfileSpec> {
  static readonly spec = Frond.resourceSpec<ProfileSpec>({
    tag: Frond.tag("resources/profile"),
    key: () => Frond.Key.singleton(),

    dependencies: Frond.dependencies(() => ({
      session: Frond.dep(SessionNode, Frond.Args.none),
      transport: Frond.dep(TransportNode, Frond.Args.none),
    })),

    driver: Frond.Driver.Async<ProfileSpec>({
      acquire: Frond.Driver.Acquire((ctx) =>
        ctx.deps.transport.result.getProfile(
          ctx.deps.session.result.userId,
          ctx.signal,
        )
      ),
    }),
  });
}

What matters here isn’t the syntax — it’s the ownership boundary. ProfileNode says what it needs before it can acquire, where cancellation enters, and where the ready result lives. React consumes the ready node instead of reconstructing its readiness logic inside a component.

import * as FrondReact from "@frondruntime/react";
import { observer } from "mobx-react-lite";

// React consumer — no readiness guards, no
// conditional queries, no local dependency dance.
// useNode returns the ready instance or suspends.
export const ProfileHeader = observer(() => {
  const profile = FrondReact.useNode(ProfileNode, Frond.Args.none);

  return <Avatar name={profile.result.displayName} />;
});

This is the category Frond is trying to name: frontend runtime as an explicit application layer, not an accident distributed across render logic and cleanup scripts.

Frond is v0, so treat the API surface as moving. The bet is stable: stop hiding runtime contracts in provider order, boolean gates, registry misses, and logout scripts. Put the graph where the graph is.

Continue