The `npx t3` bootstrap path
The published package resolves commands and configuration, locates its copied web client, and hands one readonly ServerConfig value to the layered runtime.
What this chapter resolves
- Trace npm's executable entry through command selection and server launch.
- Apply the real CLI, environment, desktop-bootstrap, and default precedence rules.
- Distinguish bundled JavaScript, copied web assets, and native external dependencies.
npx t3 looks like one command, but it crosses three distinct systems: npm
selects a published executable, the Effect CLI selects a command and resolves
configuration, and the server runtime acquires the services described in Chapter 6.
Keeping those stages separate makes several otherwise surprising behaviors obvious.
What npm actually installs
The package is named t3, exposes dist/bin.mjs as the t3 executable, and
publishes only dist. That directory is deliberately more than one JavaScript file.
| Artifact | How it gets there | Why it remains distinct |
|---|---|---|
dist/bin.mjs | bundled from src/bin.ts | the npm executable and command graph |
dist/service-launcher.mjs | second bundle entry | standalone managed-update trial and handoff process |
dist/client/* | copied from the completed web build | static web application served by the CLI |
selected node_modules | kept external by policy | native binaries/loaders must exist on the real filesystem |
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
The source CLI and service launcher become separate files in dist. The web build is copied to dist/client. Native packages remain in installed node_modules. package.json maps the t3 executable to dist/bin.mjs and publishes only dist.
One entry, several command meanings
const connectPublicConfigMissingMessage =
"T3 Connect commands are unavailable: this build is missing T3 Connect public configuration.";
class ConnectPublicConfigMissingError extends CliError.UserError {
override get message() {
return connectPublicConfigMissingMessage;
}
}
const connectUnavailableCommand = Command.make("connect", {
command: Argument.string("command").pipe(Argument.variadic),
}).pipe(
Command.withDescription("T3 Connect is unavailable in builds without public configuration."),
Command.withHidden,
Command.withHandler(() =>
Effect.fail(
new CliError.ShowHelp({
commandPath: ["t3", "connect"],
errors: [new ConnectPublicConfigMissingError({ cause: connectPublicConfigMissingMessage })],
}),
),
),
);
export const makeCli = ({ cloudEnabled = hasCloudPublicConfig } = {}) =>
Command.make("t3", { ...sharedServerCommandFlags }).pipe(
Command.withDescription("Run the T3 Code server."),
Command.withHandler((flags) => runServerCommand(flags)),
Command.withSubcommands([
startCommand,
serveCommand,
pairCommand,
authCommand,
projectCommand,
serviceCommand,
servicePreflightCommand,
triageCommand,
cloudEnabled ? connectCommand : connectUnavailableCommand,
]),
);
export const cli = makeCli();
if (import.meta.main) {
Command.run(cli, { version: packageJson.version }).pipe(
Effect.scoped,
Effect.provide(CliRuntimeLayer),
NodeRuntime.runMain,
);Bare t3 and t3 start both call runServerCommand with normal browser
presentation. t3 serve is not merely an alias for --no-browser: it requests
headless presentation and forces cwd auto-bootstrap off so a service can start
without inventing project state.
export const startCommand = Command.make("start", { ...sharedServerCommandFlags }).pipe(
Command.withDescription("Run the T3 Code server."),
Command.withHandler((flags) => runServerCommand(flags)),
);
export const serveCommand = Command.make("serve", { ...sharedServerCommandFlags }).pipe(
Command.withDescription(
"Run the T3 Code server without opening a browser and print headless pairing details.",
),
Command.withHandler((flags) =>
runServerCommand(flags, {
startupPresentation: "headless",
forceAutoBootstrapProjectFromCwd: false,
}),
),Follow `npx t3` into the runtime
Use Previous/Next or select a stage; the moving token stops where ownership changes.
Step 1 of 7: Resolve
npm selects the published executable
The package bin map points the t3 command at dist/bin.mjs. The copied client and launcher are sibling runtime artifacts, not source-tree assumptions.
- 1. Resolve · npm selects the published executable (Transport boundary)
The package bin map points the t3 command at dist/bin.mjs. The copied client and launcher are sibling runtime artifacts, not source-tree assumptions.
apps/server/package.json:1–52↗apps/server/scripts/cli.ts:225–304↗ - 2. Dispatch · The Effect CLI selects a handler (Runtime work)
Bare, start, serve, auth, pair, project, service, hidden __service-preflight, triage, and connect share one typed command graph. Connect is functional or hidden-unavailable according to the compiled cloud configuration.
apps/server/src/bin.ts:23–71↗apps/server/src/cli/servicePreflight.ts:7–16↗apps/server/src/cli/server.ts:8–35↗ - 3. Resolve config · Inputs collapse into one ServerConfig (Durable state)
Each field selects from the channels that actually apply to it; the resolver also creates required directories and constructs one readonly service value.
apps/server/src/cli/config.ts:20–191↗apps/server/src/cli/config.ts:210–392↗ - 4. Locate UI · Development and packaged clients diverge (Client state)
A dev URL disables eager staticDir resolution. Loopback requests redirect to the development server; other eligible requests may still lazily locate the built client. Without a dev URL, lookup is eager.
apps/server/src/config.ts:206–225↗apps/server/src/http.ts:233–322↗ - 5. Launch · Configuration becomes an Effect service (Runtime work)
runServerCommand provides the resolved ServerConfig to runServer. Layer acquisition now owns the process lifetime and registers activation-aware roots.
apps/server/src/cli/server.ts:8–35↗apps/server/src/server.ts:474–689↗ - 6. Prepare · The listener and parked roots reach the activation boundary (Runtime work)
Presentation is registered behind the same gate. Startup waits for the listener and auxiliary roots, prepares the launcher trial, publishes welcome, and then activates.
apps/server/src/serverRuntimeStartup.ts:385–583↗apps/server/src/serverActivation.ts:11–25↗ - 7. Present · Presentation mode chooses how access is exposed (Transport boundary)
Web browser presentation resolves a pairing target and honors noBrowser; desktop resolves its base URL. Headless output alone reads the bound port, combines it with configured host policy, and prints a credential, fragment URL, and QR code.
apps/server/src/serverRuntimeStartup.ts:257–288↗apps/server/src/startupAccess.ts:71–147↗
Configuration precedence is field-specific
The common helper selects the first present Option, but callers supply different
subsets of channels and defaults. Mode and port consult CLI, environment, desktop
bootstrap, and a default; dev URL has CLI/environment inputs; cwd uses CLI/process
cwd; presentation comes from the selected command. “CLI always wins” is therefore
directionally useful, not a complete specification.
const mode: ServerConfig.RuntimeMode = Option.getOrElse(
resolveOptionPrecedence(
normalizedFlags.mode,
Option.fromUndefinedOr(env.mode),
Option.fromUndefinedOr(bootstrap?.mode),
),
() => "web",
);
const port = yield* Option.match(
resolveOptionPrecedence(
normalizedFlags.port,
Option.fromUndefinedOr(env.port),
Option.fromUndefinedOr(bootstrap?.port),
),
{
onSome: (value) => Effect.succeed(value),
onNone: () => {
if (mode === "desktop") {
return Effect.succeed(ServerConfig.DEFAULT_PORT);
}
return findAvailablePort(ServerConfig.DEFAULT_PORT);
},
},
);
const devUrl = Option.getOrElse(
resolveOptionPrecedence(normalizedFlags.devUrl, Option.fromUndefinedOr(env.devUrl)),
() => undefined,
);
const explicitBaseDir = resolveOptionPrecedence(
normalizedFlags.baseDir,
Option.fromUndefinedOr(env.t3Home),
).pipe(Option.filter((value) => value.trim().length > 0));
const baseDir = yield* resolveBaseDir(
Option.getOrUndefined(
resolveOptionPrecedence(explicitBaseDir, Option.fromUndefinedOr(bootstrap?.t3Home)),
),Resolve four startup configurations
Choose an input set and inspect the winning channel and downstream consequence.
Scenario 1 of 4: Web defaults
Prefer 3773, then ask the OS for an ephemeral port
With no explicit mode or port, the CLI uses web mode and tries the default once before reserving an OS-assigned loopback port.
- cwd defaults to the invoking process and is normalized to an absolute path.
- Browser presentation is enabled by default.
- cwd project auto-bootstrap defaults on in web mode.
- An undefined host becomes the concrete loopback bind 127.0.0.1 in both platform implementations.
- Web defaults · Prefer 3773, then ask the OS for an ephemeral port (Expected path)
With no explicit mode or port, the CLI uses web mode and tries the default once before reserving an OS-assigned loopback port.
- cwd defaults to the invoking process and is normalized to an absolute path.
- Browser presentation is enabled by default.
- cwd project auto-bootstrap defaults on in web mode.
- An undefined host becomes the concrete loopback bind 127.0.0.1 in both platform implementations.
apps/server/src/cli/config.ts:210–392↗packages/shared/src/Net.ts:194–200↗apps/server/src/config.ts:99–155↗apps/server/src/server.ts:123–240↗ - CLI beats env · Explicit flags win their fields (Alternative)
A CLI mode, port, host, or base directory is selected before the corresponding environment and bootstrap values.
- Precedence is evaluated independently per field.
- An explicit base directory changes how state paths are derived.
- Supplying a dev URL disables eager static lookup; non-loopback fallback may still resolve built assets lazily.
apps/server/src/cli/config.ts:20–191↗apps/server/src/cli/config.ts:210–392↗apps/server/src/config.ts:99–155↗ - Desktop bootstrap · The parent can inject one-time process configuration (Caveat)
When no higher-precedence value exists, a bootstrap envelope supplies mode, port, host, T3 home, telemetry descriptors, and presentation-related settings.
- The bootstrap envelope is read only when a bootstrap fd is configured.
- The Electron parent normally chooses a port before placing it in that envelope.
- Standalone CLI desktop-mode fallback is exactly port 3773; desktop defaults to loopback host and no browser.
apps/server/src/cli/config.ts:210–392↗apps/server/src/cli/config.ts:20–191↗apps/desktop/src/app/DesktopApp.ts:69–103↗apps/desktop/src/backend/DesktopBackendConfiguration.ts:366–414↗ - Headless serve · The command overrides presentation intent (Expected path)
Serve forces headless output and disables automatic cwd project creation even if ordinary web defaults would enable it.
- No browser is opened.
- The output uses the listener's actual port, not only the configured fallback.
- The pairing credential is placed in the URL fragment.
apps/server/src/cli/server.ts:8–35↗apps/server/src/startupAccess.ts:71–147↗
State paths and client selection
The resolver derives paths only after the base directory and dev URL are known,
creates the cwd and server directories, loads persisted observability endpoints, and
then returns the effective readonly ServerConfig value. A dev URL enables a
loopback redirect and disables eager static lookup; a non-redirected eligible request
can still resolve built assets lazily. Without a dev URL, lookup eagerly checks the
compiled-adjacent client directory before the monorepo fallback. If neither has an
index, the catch-all route returns HTTP 503 instead of an application shell.