Server composition, activation, and readiness
Effect layers acquire the server graph, while an explicit activation barrier separates listening, parked roots, command admission, and lifecycle readiness.
What this chapter resolves
- Read the server as a scoped layer graph rather than one startup function.
- Distinguish an open listener from an activated, command-ready runtime.
- Trace startup failure and shutdown ownership without inventing transactional guarantees.
Chapter 5 ended with one resolved ServerConfig. It does not get passed to a
constructor that imperatively starts everything. runServer launches an Effect
layer whose acquisition builds a graph of platform services, application services,
routes, long-running roots, and release finalizers. A second mechanism—the
activation barrier—controls when already-acquired roots may begin external work.
The server is a scoped graph
Zoom with the controls, +/−, or Ctrl/⌘ + trackpad scroll. Enable Pan to drag, use two-finger scrolling, or use the arrow keys. 0 fits the diagram; Esc leaves Pan or expanded view.
Text equivalent
ServerConfig selects Bun or Node platform services. Platform and SQLite services support persistence, authentication, settings, orchestration, providers, VCS, checkpoints, terminals, preview, relay, and telemetry. Those application services support HTTP, WebSocket RPC, MCP, and static routes. The HTTP server owns the assembled graph for one Effect scope. An activation deferred is a separate gate shared by long-running roots.
The graph is broad because the environment server is the execution authority from
Chapter 1. Composition still preserves boundaries: each service exposes a typed
interface, layers declare requirements, and the launched scope owns acquisition and
release. makeServerLayer is the composition root; Layer.launch keeps it alive.
SQLite is acquired before command traffic
The SQLite layer configures a busy timeout, enables foreign keys, selects WAL, and runs a statically ordered migration manifest during acquisition. At this revision the manifest contains 41 migrations. That number is a fact about the pinned source, not a compatibility promise for later revisions.
Four signals that sound similar but are not
| Signal | What it proves | What it does not prove |
|---|---|---|
| HTTP listening | the platform server has bound an address | commands are accepted or auxiliary roots are active |
| parked / routes ready | each participating root reached its pre-activation wait point | the activation gate has opened |
| command ready | the global route barrier and startup command queue may release waiting effects | every later lifecycle subscriber has observed ready |
ready event | activation and command admission have occurred and startup publishes its final lifecycle payload | all future optional integrations will remain healthy |
The listener can therefore exist while every route effect waits behind global middleware; a separate FIFO startup queue also holds WebSocket-dispatched commands. That is intentional: routes need a real bound address before presentation and remote-access setup can be prepared, but external work must not race ahead of a candidate runtime that has not crossed its activation boundary.
yield* Effect.logDebug("startup phase: waiting for http listener");
yield* runStartupPhase("http.wait", Deferred.await(httpListening));
yield* runStartupPhase(
"auxiliary-roots.parked",
options?.awaitAuxiliaryParked ?? Effect.void,
);
// This is the prepared boundary. Every dependency has been acquired and
// every runtime root has confirmed that it is parked before this request.
const updateOutcome = yield* launcher.prepareTrial;
yield* runStartupPhase(
"welcome.publish",
lifecycleEvents.publish({
version: 1,
type: "welcome",
payload: { environment, ...welcomeBase },
}),
);
yield* options?.activate ?? Effect.void;
yield* Effect.logDebug("Accepting commands");
yield* commandGate.signalCommandReady;
yield* runStartupPhase(
"ready.publish",
lifecycleEvents.publish({
version: 1,
type: "ready",
payload: {
at: DateTime.formatIso(yield* DateTime.now),
environment,
...(updateOutcome === undefined ? {} : { updateOutcome }),
},
}),
);
yield* Effect.logDebug("startup phase: complete");Open the server in the order the code does
Play the one-shot sequence or select any boundary. Notice that listening arrives before activation, while command readiness arrives after it.
Step 1 of 7: Acquire
Layers acquire dependencies and SQLite
The selected platform, database pragmas and migrations, application services, routes, and scoped resources are constructed. Acquisition failure prevents a usable runtime.
- 1. Acquire · Layers acquire dependencies and SQLite (Durable state)
The selected platform, database pragmas and migrations, application services, routes, and scoped resources are constructed. Acquisition failure prevents a usable runtime.
apps/server/src/server.ts:123–240↗apps/server/src/server.ts:242–429↗apps/server/src/persistence/Layers/Sqlite.ts:19–41↗apps/server/src/persistence/Migrations.ts:15–152↗ - 2. Park roots · Long-running roots start but await activation (Runtime work)
The orchestration reactors and provider-session reaper are started inside a dedicated scope; activation-aware children first signal that they are parked.
apps/server/src/orchestration/Layers/OrchestrationReactor.ts:14–31↗apps/server/src/serverActivation.ts:11–25↗apps/server/src/serverRuntimeStartup.ts:385–583↗ - 3. Reconcile · Orphaned provider state is repaired best-effort (Durable state)
Startup reconciles persisted sessions that cannot still be live after process loss. This precedes welcome publication and command admission.
apps/server/src/serverRuntimeStartup.ts:385–583↗ - 4. Listen · HTTP and every auxiliary participant report prepared (Transport boundary)
The HTTP layer marks its bound listener; runtime state, routes, cloud link, and optional Tailscale roots reach their parked points. Route effects still wait behind the readiness barrier.
apps/server/src/server.ts:474–689↗apps/server/src/server.ts:431–437↗apps/server/src/serverRuntimeStartup.ts:385–583↗ - 5. Prepare trial · The managed launcher evaluates the candidate (Runtime work)
Only at the prepared boundary does startup ask the service launcher to prepare a managed-update trial and capture its outcome.
apps/server/src/serverRuntimeStartup.ts:385–583↗ - 6. Activate · Welcome publishes and the shared gate opens (External side effect)
Startup publishes the base welcome payload, then resolves the activation deferred. Parked roots may now perform their work and activation-owned side effects may begin.
apps/server/src/server.ts:474–689↗apps/server/src/serverRuntimeStartup.ts:385–583↗ - 7. Admit · Commands are released, then ready is published (Client state)
The command gate is signaled before the ready lifecycle event. Startup failures instead fail the command gate and invoke the layer's abort path.
apps/server/src/serverRuntimeStartup.ts:94–136↗apps/server/src/serverRuntimeStartup.ts:385–583↗
After activate, the parked fibers are released concurrently. Heartbeat,
browser/headless presentation, optional auto-bootstrap welcome, runtime-state
persistence, and remote-access work may interleave with command admission and the
ready publication. The flow orders the gate operations; it does not serialize all
post-activation side effects.
Why park before activating?
forkParked is small but architectural. Its child fiber first fulfills a “parked”
deferred and then waits on ServerActivation. The parent can prove all participating
roots exist and are waiting before it releases a single shared gate. That prevents
one early root from acting against a partially prepared graph.
This is synchronization, not a global transaction. Once activation opens, roots can perform filesystem, provider, Tailscale, cloud, browser, or other side effects with their own failure handling. Effect scopes provide release ownership; they do not magically roll back every effect that crossed an external boundary.
Inject a startup or shutdown condition
Choose a condition to see what the barrier guarantees and what remains best-effort.
Scenario 1 of 4: Settings warning
Startup deliberately degrades and continues
Keybinding and settings start errors are caught, logged as warnings, and do not fail the activation sequence.
- The runtime may become ready with a reported configuration issue.
- This exception is local to those startup phases, not a blanket ignore policy.
- Later settings operations still use their typed service errors.
- Settings warning · Startup deliberately degrades and continues (Caveat)
Keybinding and settings start errors are caught, logged as warnings, and do not fail the activation sequence.
- The runtime may become ready with a reported configuration issue.
- This exception is local to those startup phases, not a blanket ignore policy.
- Later settings operations still use their typed service errors.
apps/server/src/serverRuntimeStartup.ts:385–583↗ - Before prepared · A required failure prevents activation (Failure path)
If a non-degraded startup phase fails before the prepared boundary, the startup fiber records a ServerRuntimeStartupError.
- Command readiness is failed, so waiting command effects fail instead of hanging indefinitely.
- The abort hook fails the activation deferred.
- The surrounding server scope owns acquired-resource cleanup.
apps/server/src/serverRuntimeStartup.ts:94–136↗apps/server/src/serverRuntimeStartup.ts:385–583↗apps/server/src/server.ts:474–689↗ - After activation · Individual roots own later resilience (Caveat)
Activation establishes a common start boundary, not permanent health of every auxiliary integration.
- Tailscale and cloud-link paths catch and log selected failures.
- Commands are already admitted after the shared gate opens.
- Service-specific retry and cleanup policies determine later behavior.
apps/server/src/server.ts:474–689↗apps/server/src/serverRuntimeStartup.ts:385–583↗ - Scope closes · Release finalizers run under Effect ownership (Expected path)
Closing the launched scope closes the reactor scope and releases acquired resources such as persisted runtime state and configured Tailscale Serve.
- Registered finalizers run according to their owning scopes.
- The composition root does not assemble one production-wide atomic drain across every external side effect.
- External systems may require their own compensating cleanup.
apps/server/src/server.ts:474–689↗apps/server/src/serverRuntimeStartup.ts:385–583↗