Skip to main content

CNStra: A New Generation of Application Orchestration

Brain in Grass

In one line: CNStra is a single-process orchestration engine for your app's logic. You model behavior as an explicit, typed graph of small, isolated handlers ("neurons") wired by named signals ("collaterals"). A signal propagates through the graph deterministically; every step is traceable (and a run can be persisted and resumed); and because every connection is a typed edge, the compiler shows you the blast radius of any change. It also gives you the knobs to control how a flow runs — concurrency, queues, retries, timeouts, cancellation — and stays decoupled from storage and I/O, so the same model runs on the backend and the frontend. It is not a state manager, not an event bus, and not a distributed system.

The rest of this page shows why it's shaped this way — starting from where the usual patterns break.

1. MVC — Clean on Paper, Chaotic in Reality

Modern apps are no longer CRUD.

They are workflows: multi‑step sequences that must remain consistent across UI, onboarding, background sync, imports, batch jobs, and automation.

But classic patterns treat flows as second‑class citizens.

For example, on paper, MVC looks clean:

  • Controllers call model methods,
  • Models update data,
  • Views rerender.

But the moment you create your first multi-step operation, the problems jump out at you.

Flow: "Create a deck, then create a card inside it."

Controller

function onCreateCardClick(req) {
const deck = Deck.create({ title: req.deckTitle });
const card = Card.create({ deckId: deck.id, title: req.cardTitle });
}

Models

class Deck {
static create(data) {
return db.insert("decks", data);
}
}

class Card {
static create(data) {
return db.insert("cards", data);
}
}

That's it. Neither model has any idea:

  • why it was created,
  • who initiated the sequence,
  • what other operations the flow includes,
  • which parent flow it belongs to.

There is no flow identity, no graph, no traceability.

And when business later says:

  • "We need the same operation during onboarding"
  • "And during import"

—you now have multiple scattered controller copies of the same flow. Nothing ensures they stay consistent.

MVC collapses the moment flows stop being trivial.


2. REDUX — MVC Problem Solved... Or Is It?

Faced with the same "create a deck with a card" task, most people arrive at roughly this:

Redux Toolkit reducers

import { createSlice, createEntityAdapter } from '@reduxjs/toolkit';

const deckAdapter = createEntityAdapter();
const cardAdapter = createEntityAdapter();

const deckSlice = createSlice({
name: 'deck',
initialState: deckAdapter.getInitialState(),
reducers: {
cardWithDeckCreated: (state, action) => {
deckAdapter.addOne(state, action.payload.deck);
},
},
});

const cardSlice = createSlice({
name: 'card',
initialState: cardAdapter.getInitialState(),
reducers: {
cardWithDeckCreated: (state, action) => {
cardAdapter.addOne(state, action.payload.card);
},
},
});

Flow thunk

export const createCardWithDeck =
(deckTitle, cardTitle) => (dispatch) => {
const deck = { id: nanoid(), title: deckTitle };
const card = { id: nanoid(), deckId: deck.id, title: cardTitle };

dispatch({
type: 'cardWithDeckCreated',
payload: { deck, card },
});
};

In essence, you've recreated MVC: the models (reducers) know nothing about the flow, and the controller (thunk) just calls them in sequence.

Redux handles independent state updates well — the case where there's no flow to coordinate. Add a flow, though, and you're back to a thunk driving reducers by hand.

But flows? Still invisible.


3. MODERN STATE MANAGERS — Amazing Reactivity, Zero Orchestration

Zustand, Jotai, Recoil, MobX, Signals, Effector, Valtio…

All fantastic tools — but they solve reactivity, not flows.

Architecturally, they are Reactive MVC:

  • atoms/stores/signals = Model,
  • components = View,
  • async helpers = ad-hoc controllers.

And exactly like MVC:

  • state units have no idea what flows they participate in,
  • controllers become an unstructured soup of async logic,
  • multi-step operations are just naked functions.
createDeckAndCard();

Then onboarding calls it, import calls it, test suite calls it, background sync calls it.

Nothing describes a flow. Nothing guarantees consistency. Nothing warns you that you forgot to wire a subflow to a new parent.

Reactive state ≠ orchestration.


4. CNSTRA — Flows Become Explicit, Traceable, and Safe

CNStra Art

CNStra introduces something missing everywhere else:

Flows become real, typed orchestration nodes — not just functions.

Let's revisit Deck + Card and see how the same flow can be triggered:

  • from a UI button (createCardWithDeckButtonClicked), and
  • from an onboarding flow (userEntersApp),

while keeping everything explicit.

New to the vocabulary? In one breath: a neuron is an isolated handler, a collateral is a named typed signal, a neuron's axon is the collaterals it may emit, a dendrite binds a handler to an incoming collateral, and stimulate fires a signal into the graph. See the full nervous-system analogy in the Introduction.

import { CNS, collateral, neuron, withCtx, TCNSSignal } from '../src/index';

const uiAxon = {
userEntersApp: collateral<{
userId: string;
deckTitle: string;
cardTitle: string;
}>(),
createCardWithDeckButtonClicked: collateral<{
deckTitle: string;
cardTitle: string;
}>(),
};

const deckAxon = {
createdAtUserEntersApp: collateral<{
deckId: string;
cardTitle: string;
userId: string;
}>(),
createdAtCreateCardWithDeckButtonClicked: collateral<{
deckId: string;
cardTitle: string;
}>(),
};

const deck = neuron(deckAxon)
.dendrite({
collateral: uiAxon.userEntersApp,
response: (payload, axon) => {
const deckId = 'deck-' + Math.random().toString(36).slice(2);
return axon.createdAtUserEntersApp.createSignal({
deckId,
cardTitle: payload.cardTitle,
userId: payload.userId,
});
},
})
.dendrite({
collateral: uiAxon.createCardWithDeckButtonClicked,
response: (payload, axon) => {
const deckId = 'deck-' + Math.random().toString(36).slice(2);
return axon.createdAtCreateCardWithDeckButtonClicked.createSignal({
deckId,
cardTitle: payload.cardTitle,
});
},
});

const card = neuron({})
.dendrite({
collateral: deckAxon.createdAtCreateCardWithDeckButtonClicked,
response: payload => {
console.log('card title', payload.cardTitle);
// create a card
},
})
.dendrite({
collateral: deckAxon.createdAtUserEntersApp,
response: payload => {
console.log('card title', payload.cardTitle);
// create a card
},
});

const cns = new CNS([deck, card]);

cns.stimulate(
uiAxon.userEntersApp.createSignal({
userId: 'user-123',
deckTitle: 'Deck 1',
cardTitle: 'Card 1',
})
);

cns.stimulate(
uiAxon.createCardWithDeckButtonClicked.createSignal({
deckTitle: 'Deck 1',
cardTitle: 'Card 1',
})
);
  • Each collateral has a named, typed identity.
  • A neuron is expected to emit only signals declared on its own axon — a typed convention the compiler and graph make visible, rather than a runtime lock.
  • This prevents accidental emissions and keeps wiring explicit.
  • Even if two flows share logic, they stay structurally distinct.
  • Both flows produce the same deterministic orchestration, but each carries its own flow identity.

You can build the same compile-time guarantees on top of MVC/Redux/Signals using discriminated unions, exhaustive switches, and event schemas — but that essentially means reimplementing CNStra yourself. The value here is having this as one cohesive, ready-made model rather than a hand-assembled one.


5. Exhaustive Binding — The Guarantee Everyone Else Lacks

Every other architecture suffers from the same silent failure mode:

Flows evolve. Subscribers don't. No one warns you.

CNStra eliminates this entire class of bugs.

neuron.bind(axon, handlers) enforces EXHAUSTIVENESS

const order = {
created: collateral<...>(),
updated: collateral<...>(),
cancelled: collateral<...>(),
};

const orderMailer = neuron({})
.bind(order, {
created: (p) => sendCreated(p),
updated: (p) => sendUpdated(p),
cancelled: (p) => sendCancelled(p),
});

Then the domain evolves:

order.refunded = collateral<...>();

What happens?

  • MVC / Redux / Zustand / Jotai / MobX / Signals → no warnings, silent bug.

  • CNStra → TypeScript error: missing handler for refunded.

You literally cannot ship an incomplete flow.


6. Benchmarks

📊 View Full Benchmark Report | 🔗 Interactive Results | 📦 Source Code

What we measured on three planes (one machine, fixed library versions — directional, not universal; see the full report):

  • Under React (production build), fine-grained stores tie. Cnstra + OIMDB, both MobX variants, and atomic Effector all land at ~33–36 µs/update — React's commit cost dominates and the small spread between them is noise.
  • The clearest difference we saw is fine-grained vs coarse. Coarse stores that copy the whole record and re-run all selectors (Effector-ids, Zustand, Redux Toolkit) were ~35–160× slower under React (1230 / 2372 / 5430 µs/update).
  • On the pure data layer (no React), OIMDB/Cnstra measured fastest — 0.25–0.48 µs/update, roughly 2–3× faster than MobX in our runs, while coarse stores sat at 95–302 µs. The Cnstra orchestration on top of OIMDB read as a small fixed cost (≈ noise), not a multiplier.
  • On memory, Cnstra/OIMDB had the lightest footprint we measured — 25.8–28.1 MB steady-state heap (in-place is leanest). Atomic Effector was ~3.5× heavier (89.7 MB): a store + event per entity buys update speed with memory.

Takeaway: switching between fine-grained stores won't change React throughput (React dominates) — pick on architecture, data-layer cost, and memory. In our setup Cnstra + OIMDB sat in the top tier under React, measured fastest on the data layer, and had the lightest footprint — though that's a single-machine snapshot, so verify on your own workload.


Final Summary

Most architectures:

  • expose state updates,
  • but keep flows implicit.

That's why orchestration rots.

CNStra flips the model:

  • flows become explicit (a typed graph of collaterals), not implicit call order,
  • parent/child chains are tracked automatically, so runs stay traceable,
  • compile-time exhaustiveness eliminates missing handlers,
  • execution is controllable (concurrency, queues, retries, timeouts) and can be persisted and resumed,
  • state stays in a data layer of your choice (e.g. OIMDB) — CNStra orchestrates, it doesn't store.

This is not Flux 3.0 or "better Redux".

An explicit orchestration layer for application logic — decoupled from storage and I/O, on the backend and the frontend alike.

Eye

Same primitives, same guarantees, wherever it runs:

  • workflows,
  • pipelines,
  • background jobs,
  • domain events,
  • durable, resumable runs.

If you want a deep-dive into: