# Host worlds for a team

Many worlds under one URL, each with its own token, and a console that shows them all.

This page is executed as written by `packages/cli/src/journeys/tutorials.test.ts`; the recording
is made from the same run.

![The page, recorded](../media/host-worlds-for-a-team/host-worlds-for-a-team.gif)

A host serves many shared worlds by `<org>/<world>`. Every world keeps its own token and its own
history; the host keeps one admin token that provisions and removes worlds, and mounts the console
when it is installed beside it. This page runs a host on your machine and points an app at one of
its worlds. Volter runs the same host for you when you would rather not: sign in, create an org,
provision a world — the console, the command and the twins are the same packages. For orgs,
members and sign-in through your own provider (Okta, Entra, Google Workspace, GitHub) instead of
tokens, run the platform in front of the host: Self-host the platform.

## The app

```json file=package.json
{ "name": "acme-web", "private": true, "type": "module", "dependencies": { "@octokit/rest": "^21" } }
```

```js file=file-issue.mjs
import { Octokit } from '@octokit/rest';
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const { data: issue } = await octokit.issues.create({ owner: 'acme', repo: 'web', title: process.argv[2] ?? 'Launch checklist' });
console.log(`filed #${issue.number}`);
```

```bash
npm install
npm install -g @volter/world
npm install -D @volter/twin-github
```

## The host

A host's directory holds the host, the console and the twin packages its worlds use, and one
bare world per `<org>/<world>`, laid out as the self-hosted image is. Install them, make the
first world, then serve the directory: the host prints the address it is reached at, its admin
token, and the console's URL. The admin token opens the host's own endpoints and nothing under a
world; keep it where you keep any team secret.

```bash
mkdir -p ../worlds && cd ../worlds
npm install @volter/world-host @volter/world-console @volter/twin-github
volter world init --bare acme/team --twins github --world acme/team
npx volter-host serve --dir . --port 4400 &
cd ../acme-web
```

```text
host ready  http://127.0.0.1:4400  1 world
admin token  tok_a_
console      http://127.0.0.1:4400/-/console/
```

## Provision a world through the HTTP API

A second world does not need a shell on the host. `POST /-/worlds` with the admin token makes it,
and `GET /-/worlds` lists every world the host serves with its base URL and its token.

```bash
curl -s -X POST http://127.0.0.1:4400/-/worlds -H "x-volter-token: $(cat ../worlds/.volter-host/admin)" -H 'content-type: application/json' -d '{"org":"acme","world":"staging","vendors":["github"]}'
curl -s http://127.0.0.1:4400/-/worlds -H "x-volter-token: $(cat ../worlds/.volter-host/admin)"
```

```text
"name":"acme/staging"
"name":"acme/team"
```

## Point your world at one of them

A world on a host is a shared world: `remote add` records its URL and token, and `push` sends
your changes to it as a changeset. Nothing about the app changes because the world moved onto a
host.

```bash
volter world init
volter remote add origin http://127.0.0.1:4400/acme/team --token "$(cat ../worlds/acme/team/.volter/token)"
volter world up
volter world run -- node file-issue.mjs
volter world changeset -m "The launch checklist"
volter world push
```

```text
filed #1
changeset  the-launch-checklist  1 change
pushed  the-launch-checklist  1 change → origin
```

## The console

Open `http://127.0.0.1:4400/-/console/` and paste the admin token: every world the host serves,
with a form to provision another. Open a world and its twins are listed; a twin opens on its log
and its tree, the same log `volter world log` prints. A world's own token opens the console on that
world alone.

## Remove a world

`DELETE /-/worlds/<org>/<world>` stops the world and removes its directory.

```bash
curl -s -X DELETE http://127.0.0.1:4400/-/worlds/acme/staging -H "x-volter-token: $(cat ../worlds/.volter-host/admin)"
```

```text
"removed":"acme/staging"
```

## Stop the host

The host was started in the background; stop it when you are done. Its directory keeps every
world, so the next `volter-host serve` brings them all back.

```bash
kill %1
```

<!-- playback:BEGIN — GENERATED by `bun scripts/docs-media.ts`; do not edit between markers -->

## Playback

Each command above, as the recording shows it.

<details><summary><code>npm install</code></summary>

![step 1](../media/host-worlds-for-a-team/step-01.png)

</details>
<details><summary><code>npm install -g @volter/world</code></summary>

![step 2](../media/host-worlds-for-a-team/step-02.png)

</details>
<details><summary><code>npm install -D @volter/twin-github</code></summary>

![step 3](../media/host-worlds-for-a-team/step-03.png)

</details>
<details><summary><code>mkdir -p ../worlds &amp;&amp; cd ../worlds</code></summary>

![step 4](../media/host-worlds-for-a-team/step-04.png)

</details>
<details><summary><code>npm install @volter/world-host @volter/world-console @volter/twin-github</code></summary>

![step 5](../media/host-worlds-for-a-team/step-05.png)

</details>
<details><summary><code>volter world init --bare acme/team --twins github --world acme/team</code></summary>

![step 6](../media/host-worlds-for-a-team/step-06.png)

</details>
<details><summary><code>npx volter-host serve --dir . --port 4400 &amp;</code></summary>

![step 7](../media/host-worlds-for-a-team/step-07.png)

</details>
<details><summary><code>cd ../acme-web</code></summary>

![step 8](../media/host-worlds-for-a-team/step-08.png)

</details>
<details><summary><code>curl -s -X POST http://127.0.0.1:4400/-/worlds -H "x-volter-token: $(cat ../worlds/.volter-host/admin)" -H 'content-type: application/json' -d '{"org":"acme","world":"staging","vendors":["github"]}'</code></summary>

![step 9](../media/host-worlds-for-a-team/step-09.png)

</details>
<details><summary><code>curl -s http://127.0.0.1:4400/-/worlds -H "x-volter-token: $(cat ../worlds/.volter-host/admin)"</code></summary>

![step 10](../media/host-worlds-for-a-team/step-10.png)

</details>
<details><summary><code>volter world init</code></summary>

![step 11](../media/host-worlds-for-a-team/step-11.png)

</details>
<details><summary><code>volter remote add origin http://127.0.0.1:4400/acme/team --token "$(cat ../worlds/acme/team/.volter/token)"</code></summary>

![step 12](../media/host-worlds-for-a-team/step-12.png)

</details>
<details><summary><code>volter world up</code></summary>

![step 13](../media/host-worlds-for-a-team/step-13.png)

</details>
<details><summary><code>volter world run -- node file-issue.mjs</code></summary>

![step 14](../media/host-worlds-for-a-team/step-14.png)

</details>
<details><summary><code>volter world changeset -m "The launch checklist"</code></summary>

![step 15](../media/host-worlds-for-a-team/step-15.png)

</details>
<details><summary><code>volter world push</code></summary>

![step 16](../media/host-worlds-for-a-team/step-16.png)

</details>
<details><summary><code>curl -s -X DELETE http://127.0.0.1:4400/-/worlds/acme/staging -H "x-volter-token: $(cat ../worlds/.volter-host/admin)"</code></summary>

![step 17](../media/host-worlds-for-a-team/step-17.png)

</details>
<details><summary><code>kill %1</code></summary>

![step 18](../media/host-worlds-for-a-team/step-18.png)

</details>

<!-- playback:END -->
