Part II · Boot and connectServer activation
Chapter 6source checked

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

Figure 6.1 · Layer acquisition and runtime ownershipsolid arrows provide dependencies; dotted arrows open parked roots
T3 server layer graphDiagram loading

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.

T3 server layer graph
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.

Figure 6.1. Platform services support durable and runtime services; those services support HTTP, WebSocket RPC, MCP, and static routes. Activation crosses the graph separately, after every required root has reported that it is parked.

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

Distinct synchronization signals in server startup
SignalWhat it provesWhat it does not prove
HTTP listeningthe platform server has bound an addresscommands are accepted or auxiliary roots are active
parked / routes readyeach participating root reached its pre-activation wait pointthe activation gate has opened
command readythe global route barrier and startup command queue may release waiting effectsevery later lifecycle subscriber has observed ready
ready eventactivation and command admission have occurred and startup publishes its final lifecycle payloadall 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.

apps/server/src/serverRuntimeStartup.ts:514–548 ↗verbatim · typescript · 8ed6988b
      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");
Read this as: The prepared boundary occurs only after the listener and auxiliary roots are parked. The launcher trial, base welcome, activation, command gate, and ready event then happen in that order.
Interactive flow

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.

1 / 7

Step 1 of 7: Acquire

Effect layersDurable stateAcquire

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. 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–240apps/server/src/server.ts:242–429apps/server/src/persistence/Layers/Sqlite.ts:19–41apps/server/src/persistence/Migrations.ts:15–152
  2. 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–31apps/server/src/serverActivation.ts:11–25apps/server/src/serverRuntimeStartup.ts:385–583
  3. 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. 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–689apps/server/src/server.ts:431–437apps/server/src/serverRuntimeStartup.ts:385–583
  5. 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. 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–689apps/server/src/serverRuntimeStartup.ts:385–583
  7. 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–136apps/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.

Interactive lab

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

Settings warningCaveat

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.
  1. 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
  2. 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–136apps/server/src/serverRuntimeStartup.ts:385–583apps/server/src/server.ts:474–689
  3. 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–689apps/server/src/serverRuntimeStartup.ts:385–583
  4. 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–689apps/server/src/serverRuntimeStartup.ts:385–583
T3
Source-locked editionRead against fa219001d · 23 Aug 2026
Book search

Find a concept, module, or source path

Type two or more characters.