Backend ivue — the same invariant on a server
Nothing in ivue mentions a screen. The engine sits on @vue/reactivity — a standalone dependency-graph library — and Reactive() transforms class prototypes, not components. The "Vue" everyone associates with browsers is the renderer, which ivue never touches. Importing vue in a DOM-less Node/Bun process is fine: runtime-dom touches document only when you createApp().mount(), which a backend never calls.
That makes ivue a server domain-model layer: reactive state with class ergonomics, running headless.
The proof is not hypothetical. Invar — a full terminal IDE — is hundreds of ivue classes running in a Bun process with no DOM: file watchers, search engines, undo coordinators, PTY servers. Its headless services (a warm app server for agent driving, an SSH channel server) are backend ivue in production shape.
Why a reactivity graph belongs on a server
The chronic backend failure modes — stale caches, forgotten invalidation, leaked subscriptions, config drift after reload — are all one defect: someone stored a derived value and then had to remember to update it. A reactivity graph deletes the remembering. Derived state is never stored; it is computed from tracked sources, and effects fire exactly when their inputs change.
An Express app is, in large part, a pile of "when X changes, remember to update Y" written by hand. ivue makes forgetting impossible, because Y was never stored.
And the cost model transfers with it: computeds are lazy and observation-priced, so the unpolled health endpoint and the unqueried cache cost nothing at rest. The engine core (alien-signals push-pull propagation) stops cascades the moment a computed re-evaluates equal — the right shape for servers, where high-frequency writes (request counters, queue depths) feed mostly-stable deriveds.
Pattern 1 — config that propagates instead of restarting
import { Reactive } from 'ivue';
import { ref, watchEffect } from 'vue';
class $ServerConfig {
get raw() {
return ref<RawConfig>(loadConfigFile());
}
// Derived config is PLAIN getters — always current, zero storage.
get databaseUrl() {
return this.raw.value.database.url;
}
get rateLimit() {
return this.raw.value.limits.requestsPerMinute;
}
reload() {
this.raw.value = loadConfigFile(); // one write; the graph does the rest
}
}
export namespace ServerConfig {
export const $Class = $ServerConfig;
export let Class = Reactive($Class);
}
const config = new ServerConfig.Class();
fs.watch(CONFIG_PATH, () => config.reload());
// Reprovision the pool when — and only when — the URL actually changes.
watchEffect(() => reconnectPool(config.databaseUrl));Every consumer reads config.rateLimit as a plain getter and is always current. The "hot reload subsystem" collapses into the dependency graph you already have.
Pattern 2 — caches whose invalidation is structural
A computed() over source state is a cache that invalidates exactly when its inputs change — never stale, never over-flushed, lazy until read.
For keyed data (users by id, tenants by name), use the Keyed Version Signals shape: plain Maps of refs, get-or-create on read, peek-only bump on write.
class $PermissionsCache {
protected readonly userVersions = new Map<UserId, Ref<number>>();
/** WRITE path: peek-only — unobserved users allocate nothing. */
invalidateUser(userId: UserId) {
const versionRef = this.userVersions.get(userId);
if (versionRef) versionRef.value++;
}
/** READ path: get-or-create, then subscribe. */
protected trackUser(userId: UserId) {
let versionRef = this.userVersions.get(userId);
if (!versionRef) {
versionRef = ref(0);
this.userVersions.set(userId, versionRef);
}
void versionRef.value;
}
get matrixFor() {
return (userId: UserId) => {
this.trackUser(userId);
return buildPermissionMatrix(userId); // re-derives only after a bump
};
}
}Write to user #42 and only computations that read user #42 recompute — on their next read, not eagerly. This is the hand-rolled dirty flag that someone always forgets to set, made unforgettable.
Pattern 3 — live queries over SSE/WebSocket
The highest-leverage pattern: a subscription is an effect, and the effect tracked whatever the query read.
import { effectScope, watchEffect } from 'vue';
function subscribe(socket: WebSocket, query: () => unknown) {
const scope = effectScope();
scope.run(() => {
watchEffect(() => {
socket.send(JSON.stringify(query())); // tracks every leaf it reads
});
});
socket.on('close', () => scope.stop()); // ALL watchers die in one call
}Any mutation touching the leaves that query read re-runs the effect and pushes the update — a live-query engine (the thing realtime platforms sell) in a dozen lines. And the part that is usually a leak farm — per-connection teardown — is one scope.stop().
Coalesce at the effect boundary (batch the push, debounce the reprovision): dispatch is nanoseconds, but effects are your code, and backend triggers arrive at network rate, not keystroke rate.
Pattern 4 — operational state that reports itself
Health endpoints read plain getters over live counters. A circuit breaker is a watcher flipping a ref, and every request path that reads isAvailable sees it instantly — no event bus, no polling loop.
class $UpstreamHealth {
get failureRate() {
return ref(0);
}
get tripped() {
return ref(false);
}
get isAvailable() {
return !this.tripped.value;
}
constructor() {
this.$watch(
() => this.failureRate.value,
(rate) => this.onRateChanged(rate),
);
}
onRateChanged(rate: number) {
if (rate > 0.5) this.tripped.value = true;
}
}The discipline that changes: every instance is an outliving instance
On a server there is no component scope to reap anything. The rules from Lifecycle & Teardown apply everywhere, not just to singletons:
- Watchers go through
this.$watch/this.$watchEffect(the instance's own scope) or an expliciteffectScopeyou own — never barewatchhoping something cleans up. - Every instance has a disposal owner that calls
$stopEffects()(orscope.stop()), usually tied to the resource lifetime it serves: a connection, a job, a tenant session. - Async results need generation tokens: a query result can arrive after the state it described has changed. Stamp requests, reject stale callbacks.
These rules were not invented for the backend — they were forced early by running an entire IDE outside the browser. They transfer verbatim.
The boundary: one process
The graph is per-process, in-memory. It does not replace Redis or your queue — it sits behind one server's slice of them: a pub/sub subscriber bumps refs on incoming messages, and reactivity fans out locally to every derived value, cache, and live subscription on that node. Cross-process propagation is the substrate's job; in-process propagation is where the hand-written "remember to update" code lived, and that is what the graph deletes.
Where this sits in the ecosystem
In-process reactive server state has one aging precedent (MobX domain models on Node), one adjacent-but-different paradigm (RxJS — event-stream composition, not derived state), and industrial-strength proofs in other languages (Jane Street's Incremental, rust-analyzer's salsa). The database-layer cousins (incremental view maintenance, realtime sync platforms) sell the same invariant as infrastructure, at infrastructure cost. ivue occupies the unclaimed middle: class-grammar signals for server domain models, with the lifecycle discipline already worked out, on the fastest signal core in the JavaScript ecosystem.