Instances and isolation

See two worlds share one runtime and config while keeping provider routes, service state, clocks, logs, and lifecycle ownership separate.

An instance is one mutable service world. A runtime applies the same resolved config to every instance it creates.

Prove two worlds are different

This checked test creates two instances on one runtime, verifies their service URLs share an origin but not a path, writes Grace only to the first, and reads both through provider-shaped HTTP:

test/instance-isolation.test.ts
import { createTestRuntime } from "localhost2137/testing";
import { expect, it } from "vitest";
import config from "../localhost.config.js";

it("isolates service state and routes for two instances of one config", async () => {
	const runtime = await createTestRuntime({
		config,
		port: 0,
		storage: "temporary",
	});

	try {
		const first = await runtime.createInstance();
		try {
			const second = await runtime.createInstance();
			try {
				const firstUrl = new URL(first.slack.connection.apiUrl);
				const secondUrl = new URL(second.slack.connection.apiUrl);
				expect(firstUrl.origin).toBe(secondUrl.origin);
				expect(firstUrl.pathname).not.toBe(secondUrl.pathname);
				expect(firstUrl.pathname.endsWith("/slack/api/")).toBe(true);
				expect(secondUrl.pathname.endsWith("/slack/api/")).toBe(true);

				await first.slack.createUser({ name: "Grace" });

				await expect(readUserNames(first.slack.connection)).resolves.toEqual([
					"localhost2137-bot",
					"Grace",
				]);
				await expect(readUserNames(second.slack.connection)).resolves.toEqual([
					"localhost2137-bot",
				]);
			} finally {
				await second.destroy();
			}
		} finally {
			await first.destroy();
		}
	} finally {
		await runtime.close();
	}
});

async function readUserNames(
	connection: Readonly<{ apiUrl: string; botToken: string }>,
): Promise<string[]> {
	const response = await fetch(new URL("users.list", connection.apiUrl), {
		headers: { authorization: `Bearer ${connection.botToken}` },
	});
	const payload = (await response.json()) as {
		members: Array<{ name: string }>;
		ok: true;
	};
	expect(response.status).toBe(200);
	expect(payload.ok).toBe(true);
	return payload.members.map(({ name }) => name);
}
pnpm exec vitest run test/instance-isolation.test.ts

The negative assertion is the important one: the second provider route does not see Grace. A test that only verifies distinct IDs or successful creation has not yet proved state isolation.

One template, many worlds

ConcernOwner
Host, port, server process, and control tokenRuntime
Plugin factories, service keys, credentials, callback destinations, and other plugin configRuntime template
Plugin records and service storageInstance and service
Clock, seed status, logs, and tracked plugin workInstance
Provider API URL and app-facing connection valuesDerived for the selected instance

Two worlds may allocate the same provider-shaped record IDs because each plugin database is local to its instance. They still run the same plugin code and resolved configuration.

This is state isolation, not a process or security sandbox. Instances share a runtime endpoint and privileged control credential. The runtime supplies application processes only instance-scoped provider connection values. localhost run does not sanitize the process's inherited environment.

Addressing selects the world

The public route contains both instance and service keys:

Route shape
http://127.0.0.1:2137/dev/slack/api/
http://127.0.0.1:2137/review/slack/api/

Those are two worlds for the same slack mount. Passing one connection object to an SDK or HTTP client selects its world. Every setup and inspection operation must target that same instance.

If a test arranges review but the app still uses the dev URL, both paths can work while observing different state. Compare the app-facing URL before assuming an operation lost data.

Create, reset, and destroy mean different things

This CLI sequence owns one named world and exercises each transition:

pnpm exec localhost instance create lifecycle-demo --seed
pnpm exec localhost instance reset lifecycle-demo
pnpm exec localhost instance reset lifecycle-demo --seed
pnpm exec localhost instance destroy lifecycle-demo
TransitionIdentity and routeReplacement state
CreateAllocates a new instance ID and route.Empty unless --seed or { seed: true }.
ResetKeeps the same ID, route, and persistence policy.Replaces service state and clock; empty unless seed is explicit.
DestroyRetires the ID and route.Removes stored instance state; typed handles become unusable.

Reset is useful when one owner deliberately reuses a named development world. Independent tests usually create independent instances instead. Cleanup belongs in finally, as the checked test shows; “ephemeral” does not mean fire-and-forget.

Seed test worlds covers successful and failed seeded transitions.

Persistence answers only restart behavior

Persistence determines whether the runtime restores an instance after restart. It does not change provider behavior or widen the isolation boundary.

  • localhost dev creates persistent dev; restarting the daemon preserves it until reset/destroy.
  • createTestRuntime creates ephemeral worlds inside owned temporary storage; closing the runtime removes that storage after its instances are destroyed.
  • A remote worker can create an ephemeral world on a shared runtime but still owns its destruction.

The daemon or suite owns the runtime. A test or worker owns the instances it creates. An instance owner must not close a runtime shared by other workers.

Where the boundary stops

Instances isolate state when every interaction is instance-aware. They do not isolate runtime-scoped plugin config or external application state.

A callback URL defined in plugin config is shared by every instance of that mount. Creating another world does not add an instance header or rewrite that URL. Parallel callback tests need genuine provider-shaped correlation, serialization, or separately configured runtimes and receivers.

Use another instance when mutable plugin state must differ. Use another runtime when configuration, ports, or callback receivers must differ. Configuration defines the shared template, Integration testing defines ownership, and Callbacks and webhooks defines receiver routing.