Configuration

Assemble localhost.config.ts and look up discovery, defaults, service identity, environment, seed, and persistent-world behavior.

localhost.config.ts is executable project configuration and the template used by every instance in one runtime. This complete config is checked by the getting-started example:

localhost.config.ts
import { slack } from "@localhost2137/slack";
import { defineConfig } from "localhost2137";

export default defineConfig({
	clock: { mode: "pinned", startAt: "2026-01-01T00:00:00.000Z" },
	services: {
		slack: slack({
			config: {
				botToken: "xoxb-local-crash-course",
				eventsUrl: null,
				signingSecret: "local-crash-course-signing-secret",
				workspaceName: "Local workspace",
			},
			seed: {
				users: [{ id: "U_ADA", name: "Ada" }],
				channels: [{ id: "C_GENERAL", name: "general", members: ["U_ADA"] }],
			},
		}),
	},
});

defineConfig() preserves service and operation types. Runtime validation still runs before storage opens or the server binds.

Terminal 1:

pnpm exec localhost dev

Terminal 2:

pnpm exec localhost seed
pnpm exec localhost env --instance dev --format json

dev creates an empty persistent dev world when needed. seed applies the configured Slack users and channels once. env prints the application-facing connection values computed for that specific instance; the runtime control token is never included.

Runtime fields

FieldAccepted valueDefault and consequence
servicesRequired map of service keys to plugin-factory results; {} is validNo default. Every instance mounts the same map.
host"127.0.0.1", "localhost", or "::1""127.0.0.1"; non-loopback hosts are rejected.
portInteger from 1 through 655352137
storage{ dir: string }, non-empty after trimming{ dir: ".localhost2137" }; relative to the selected config file.
clock{ mode: "real" } or { mode: "pinned", startAt } with an RFC 3339 offset{ mode: "real" }; see Virtual time.
seedFunction receiving the typed scenario facadeNo scenario seed. Plugin seed data is independent.

The port: 0 used by createTestRuntime({ port: 0, storage: "temporary", config }) is an option on the test-runtime owner, not a valid localhost.config.ts port. It requests an OS-assigned port; leave config.port omitted or between 1 and 65535.

Top-level fields and plugin envelopes are strict. Unknown keys and invalid values produce CONFIG_INVALID, possibly with several paths such as $.services.slack.config.botToken.

Discovery and path resolution

Without --config, the CLI walks upward from its working directory and selects the first supported name in this order:

localhost.config.ts
localhost.config.mts
localhost.config.cts
localhost.config.js
localhost.config.mjs
localhost.config.cjs
pnpm exec localhost --config ./config/local.ts doctor --json
pnpm exec localhost --config ./config/local.ts dev
InputResolution rule
Discovered configFirst supported name found while walking upward
--config ./config/local.tsRelative to the command's working directory; discovery is skipped
storage: { dir: ".state" }Relative to the selected config file, not the command's working directory

The module must default-export a config. TypeScript, ESM, CommonJS, local imports, and top-level await follow the selected file's module form. Import failure is CONFIG_IMPORT_FAILED; a missing default export is CONFIG_DEFAULT_EXPORT_MISSING.

Config loads once when the daemon starts. Saving it does not update the running process. Restart after any change; control commands reject a daemon whose stored config fingerprint differs from the currently resolved project config.

Service keys are durable identities

In the complete file above, slack defines all four of these names:

SurfaceResult
Public route/{instance}/slack/*
CLIlocalhost describe slack, localhost exec slack ...
Typed test handleinstance.slack
Durable namespaceThe slack service state below that instance

Keys start with a lowercase letter, contain only lowercase letters, digits, or hyphens, and are at most 63 characters. _, clock, destroy, env, idle, reset, and seed are reserved. Access a hyphenated key with brackets: instance["primary-api"].

Mounting one plugin under two keys creates two independent service states in every instance. Renaming a key is therefore not a migration: the new route and state appear, the old route and operations disappear, and old stored data remains. Move data only through a deliberate migration the plugin supports.

Config and seed data are validated values

Each plugin owns schemas for its config and optional declarative seed. Defaults and transforms run during config resolution. Accepted data is copied, deeply frozen, and must end as JSON-compatible plain data: null, booleans, finite numbers, strings, dense arrays, and plain objects. Date, Map, class instances, undefined, sparse arrays, and cycles are rejected.

The runtime's received diagnostic records the value's type, not its value. Plugin-authored schema messages remain plugin-authored text, so use the issue path instead of logging a config object that may contain credentials.

Connection values and environment export

For each instance, a plugin computes typed connection values and string-valued env. Environment names must be uppercase shell names such as SLACK_API_URL. Duplicate exported names fail connection resolution and name both owners. exportEnv: false suppresses only that mount's merged environment entries; its routes, operations, and typed connection stay available.

The daemon writes the persistent dev projection to .env below storage.dir—by default, .localhost2137/.env. localhost env renders any selected instance; localhost run -- <command> injects the same projection into one child. The runtime control token is stored separately.

Callback destinations are ordinary plugin config and do not vary per instance. Parallel callback receivers therefore need an application routing rule; see Callbacks and webhooks.

Seed only when requested

Plugin seed data creates each service's baseline. The optional top-level function runs afterward and can connect those services through typed operations. This complete checked config sends one message after the Slack users and channel exist:

test/fixtures/seeding-config.ts
import { slack } from "@localhost2137/slack";
import { defineConfig } from "localhost2137";

export default defineConfig({
	clock: { mode: "pinned", startAt: "2026-01-01T00:00:00.000Z" },
	services: {
		slack: slack({
			config: {
				botToken: "xoxb-local-seeding-guide",
				eventsUrl: null,
				signingSecret: "local-seeding-guide-signing-secret",
				workspaceName: "Seeded workspace",
			},
			seed: {
				users: [{ id: "U_ADA", name: "Ada" }],
				channels: [{ id: "C_GENERAL", name: "general", members: ["U_ADA"] }],
			},
		}),
	},
	seed: async (world) => {
		await world.slack.sendMessage({
			channel: "general",
			from: "Ada",
			text: "baseline ready",
		});
	},
});

Seed runs only when requested by localhost seed, instance create --seed, instance reset --seed, or the equivalent test API. A successful or failed in-place seed cannot run again until reset. Seed test worlds covers ordering and recovery.

What changes do to existing worlds

Config change after daemon restartExisting persistent world
host or portRebind endpoint; recompute connection values.
Plugin config under the same keyStart with new validated config; retain durable service state.
Add a service keyCreate its state before the world becomes ready.
Remove a service keyRemove route, operations, and connection; retain its stored data.
Change plugin ID under an existing keyFail startup instead of opening another plugin's state.
Increase plugin stateVersionRun the plugin's update migration before start; fail if unavailable.
Decrease plugin stateVersionFail instead of downgrading stored state.
Change clockNew/reset worlds use it; existing worlds retain current clock state.
Change plugin or scenario seedNo automatic mutation; the next eligible explicit seed uses it.
Change storage.dirSelect a different storage root; old data is not moved.

Removing or renaming a service while it still owes an acknowledgement for a committed time window prevents that world from completing startup. Restore the prior service config and finish reconciliation; committed time recovery shows the exact failure contract.