# Getting started (/getting-started)



This is the whole shape before the details. You need Node.js 24 or later and pnpm. Start by creating
the runtime template your project will run:

```ts title="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"] }],
			},
		}),
	},
});
```

`services` can contain any installed emulator plugin. Slack makes this crash course concrete; the
runtime itself is service-agnostic. The config is one world template, not a list of instances.

## Install the runtime and one plugin [#install-the-runtime-and-one-plugin]

The Slack plugin uses `better-sqlite3`. In a new project, create this project-scoped pnpm policy. In
an existing workspace, merge `allowBuilds` into the existing top-level file:

```yaml title="pnpm-workspace.yaml"
allowBuilds:
  better-sqlite3: true
```

Install the runtime, plugin, runtime host peers, and the test runner used below. Add a development
script for the small application file from the next section:

```sh
pnpm add -D localhost2137 @localhost2137/slack hono@^4.13.4 zod@^4.4.3 vitest
pnpm pkg set scripts.dev="node ./src/read-workspace.ts"
```

Nothing is activated by installation. `localhost.config.ts` imports the plugin and gives its mount
the key `slack`; that key becomes the URL, CLI target, storage namespace, and typed test property.

Ignore the runtime's project-local state before starting it:

```text title=".gitignore"
.localhost2137/
```

## Run `localhost.config.ts` [#run-localhostconfigts]

Keep the runtime in its own terminal:

```sh
# Terminal 1
pnpm exec localhost dev
```

`dev` discovers `localhost.config.ts`, starts its configured emulators on loopback, and creates the
persistent `dev` instance when it does not exist. Configured seed data is never automatic. Apply it
from a second terminal:

```sh
# Terminal 2
pnpm exec localhost seed
```

The local Slack API now lives below `http://127.0.0.1:2137/dev/slack`. Stopping and restarting the
daemon preserves this world; reset or destroy it only when you mean to replace it.

## Point normal application traffic at the world [#point-normal-application-traffic-at-the-world]

Create the application file referenced by the `dev` script:

```ts title="src/read-workspace.ts"
type UsersListResponse =
	| { members: Array<{ id: string; name: string }>; ok: true }
	| { error: string; ok: false };

const apiUrl = requiredEnvironment("SLACK_API_URL");
const botToken = requiredEnvironment("SLACK_BOT_TOKEN");
const response = await fetch(new URL("users.list", apiUrl), {
	headers: { authorization: `Bearer ${botToken}` },
});
const payload = (await response.json()) as UsersListResponse;

if (!response.ok || !payload.ok) {
	throw new Error(
		`Local Slack request failed: ${"error" in payload ? payload.error : response.status}`,
	);
}

console.log(JSON.stringify(payload.members.map(({ id, name }) => ({ id, name }))));

function requiredEnvironment(name: string): string {
	const value = process.env[name];
	if (!value) throw new Error(`Missing ${name}. Run this command through localhost run.`);
	return value;
}
```

Inspect the selected instance's app-facing projection, then run the application with it:

```sh
pnpm exec localhost env --format json
pnpm exec localhost run -- pnpm dev
```

The application uses ordinary provider-shaped HTTP. `SLACK_BOT_TOKEN` is a local credential from the
config, not a provider-issued API key. `localhost run` overlays plugin connection values on the
child process's inherited environment. localhost2137 does not add the runtime control token, and it
does not remove values already present in the parent environment.

For an existing SDK or application adapter, replace only its endpoint and credential inputs with the
installed plugin's connection values. Do not move control operations into application code. See
[Run an existing application](/existing-application.md) for the longer migration path and
[Provider compatibility](/compatibility.md) before relying on a plugin's supported surface.

## Control the world outside the application [#control-the-world-outside-the-application]

Discover the operations exposed by the installed plugin, trigger one local actor, then inspect the
result:

```sh
pnpm exec localhost describe slack --json
pnpm exec localhost exec slack --help
pnpm exec localhost exec slack send-message \
  --channel general --from Ada --text "hello from a local user" --json
pnpm exec localhost exec slack list-messages --channel general --json
```

The CLI is generated from plugin operation schemas. Here `send-message` acts as Ada; it is not a fake
Slack endpoint for application code. The app remains on Slack-shaped HTTP while a developer, test,
or coding agent can arrange, trigger, and inspect the same state. [Operations and emulated
APIs](/operations-and-apis.md) develops that boundary.

## Create another isolated world [#create-another-isolated-world]

One runtime can host several path-addressed instances from the same config:

```sh
pnpm exec localhost instance create review --seed
pnpm exec localhost exec slack send-message \
  --instance review --channel general --from Ada --text "review world" --json
pnpm exec localhost env --instance review --format json
pnpm exec localhost run --instance review -- pnpm dev
pnpm exec localhost clock status --instance review --json
pnpm exec localhost clock advance 1h --instance review --json
pnpm exec localhost instance destroy review
```

The `review` route, plugin state, clock, logs, and storage are independent from `dev`. Both still use
the same plugin config and callback destination. The Slack setup above has no time-driven work, so
advancing its clock only demonstrates instance-scoped time control. The Stripe plugin has a concrete
30-day renewal example; [Virtual time and asynchronous work](/virtual-time.md) covers effects,
completion, and recovery.

## Own the same boundary in a test [#own-the-same-boundary-in-a-test]

Create a temporary runtime and destroy every resource from the scope that owns it:

```ts title="test/owned-runtime.test.ts"
import { createTestRuntime } from "localhost2137/testing";
import { expect, it } from "vitest";
import config from "../localhost.config.js";

it("arranges a user with an operation and reads it through Slack-shaped HTTP", async () => {
	const runtime = await createTestRuntime({
		config,
		port: 0,
		storage: "temporary",
	});

	try {
		const instance = await runtime.createInstance({ seed: true });
		try {
			const grace = await instance.slack.createUser({ name: "Grace" });
			const response = await fetch(new URL("users.list", instance.slack.connection.apiUrl), {
				headers: {
					authorization: `Bearer ${instance.slack.connection.botToken}`,
				},
			});

			expect(response.status).toBe(200);
			await expect(response.json()).resolves.toMatchObject({
				members: [
					{ id: "U000000", name: "localhost2137-bot" },
					{ id: grace.id, name: "Grace" },
					{ id: "U_ADA", name: "Ada" },
				],
				ok: true,
			});
		} finally {
			await instance.destroy();
		}
	} finally {
		await runtime.close();
	}
});
```

```sh
pnpm exec vitest run test/owned-runtime.test.ts
```

This test creates its own server on an OS-assigned port, requests configured seed explicitly, and
crosses Slack-shaped HTTP before asserting. Typed operations are available as
`instance.slack.<operation>()` for test setup and inspection; they do not replace the application
request the test claims to prove. [Integration testing](/testing.md) covers reuse, parallel workers,
asynchronous work, and cleanup failures.

## Continue with callbacks [#continue-with-callbacks]

The crash course keeps callbacks disabled. The [checked Slack Bolt workflow](/first-party/slack.md#bolt-wiring)
contains the complete application adapter and end-to-end test for signed Events API delivery, an SDK
response, `instance.idle()`, and guaranteed cleanup.

Read [Callbacks and webhooks](/callbacks.md) before parallel callback tests, or continue to
[Integration testing](/testing.md) to reuse runtimes safely. Use [Seed test worlds](/seeding.md) before
making a shared baseline larger.
