# Node hosts

Run a host as a program instead of a page. `@bitskiff/host-node` is the host SDK with the Node
platform and transport in place of the browser ones: same factory, same session, same channels.
The one difference is [`peer`](/docs/guides/transports/). This package bundles no WebRTC, so you
pass your own peer connection. [Peer-to-peer connections](#peer-to-peer-connections) below shows how, with werift.

## Install

Take the tarball from the [downloads page](/docs/downloads/): `npm install` takes a URL as well as
a name. This package depends on `@bitskiff/host`, so npm installs that tarball with it, both at the
top level of your `node_modules`.

```sh
npm install https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-host-node.tgz
npm pkg set type=module
```

The second line is what lets the `import` blocks below run. A package.json `npm init` made says
`"type": "commonjs"`, and Node refuses an `import` statement in a file under that.

Every example below is shown in JavaScript and TypeScript. Pick a tab and the site keeps that choice
on every page. Save the JavaScript as a `.js` file in this project and run it with `node <file>`.
Save the TypeScript as a `.ts` file and run it the same way.

Node 22.18 or later strips the types for you. Earlier Node 22 needs
`node --experimental-strip-types <file>` for the TypeScript. Node 22 or later is what
`@bitskiff/host-node` needs either way.

The first block below is a whole program. The blocks after it are snippets: each shows the lines its
section is about. The complete copy-paste programs on this site are the
[quickstart](/docs/quickstart/) and the three worked examples.

Open with the project's **secret key** (`bsk_sk_...`), read from the environment and never written
in source. A publishable key does not work here, because a Node process sends no browser origin for
the allowlist to check.

```js
import { BitskiffHost, qrTerminal } from '@bitskiff/host-node';
import { openForever } from '@bitskiff/host-node/unattended';

const bitskiff = new BitskiffHost();                  // the key comes from BITSKIFF_SECRET_KEY

const press = bitskiff.channel('press');              // declare every channel before the first open
const readout = bitskiff.channel('readout');

press.on('message', (m) => {                          // only `press` reaches this, in every session
  readout.send(m.client, `pressed ${m.data}`);        // a message is a string or bytes
});

const loop = openForever(bitskiff, {                  // opens a session, and another when one ends
  session: { hostResumeWindow: '5m' },                // what `open` takes, for every session
});

loop.on('open', (session) => {                        // every session, the first one included
  console.log(qrTerminal(session.joinCode.url));      // scannable straight out of the terminal
  console.log(session.joinCode.url);                  // and the link itself, for a machine with no camera
  session.on('joinCode', (code) => {                  // the code rotates: draw the new one
    console.log(qrTerminal(code.url));
    console.log(code.url);
  });
});
```

```ts
import { BitskiffHost, qrTerminal } from '@bitskiff/host-node';
import type { JoinCode, Message, Session } from '@bitskiff/host-node';
import { openForever } from '@bitskiff/host-node/unattended';

const bitskiff = new BitskiffHost();                  // the key comes from BITSKIFF_SECRET_KEY

const press = bitskiff.channel('press');              // declare every channel before the first open
const readout = bitskiff.channel('readout');

press.on('message', (m: Message) => {                 // only `press` reaches this, in every session
  readout.send(m.client, `pressed ${m.data}`);        // a message is a string or bytes
});

const loop = openForever(bitskiff, {                  // opens a session, and another when one ends
  session: { hostResumeWindow: '5m' },                // what `open` takes, for every session
});

loop.on('open', (session: Session) => {               // every session, the first one included
  console.log(qrTerminal(session.joinCode.url));      // scannable straight out of the terminal
  console.log(session.joinCode.url);                  // and the link itself, for a machine with no camera
  session.on('joinCode', (code: JoinCode) => {        // the code rotates: draw the new one
    console.log(qrTerminal(code.url));
    console.log(code.url);
  });
});
```

That is the quickstart's handler, unchanged. The sample client page's button sends one message
when it goes down and one when it comes up. So the client reads `pressed down` and then
`pressed up` on its own readout, the same as it does against a page.

**The join code rotates, so draw it on the event too.** A code a client sees stays accepted for at
least one full step and at most two, 30 to 60 seconds at the default. It survives to the end of the
step after its own.

The `open` event hands you the first code and `on('joinCode')` hands you every
one after it. A host that draws the code once ends up showing a code that no longer works.

Both places above draw the same way, and both print `joinCode.url` under the QR. A machine with no
camera in front of it is paired by copying that link, as in the quickstart.

**Open in the loop, not once.** Sessions end. The host grace (`hostResumeWindow`) is how long a
session waits for its host to come back after its link drops. A link that stays down past the host
grace ends the session, and so does a close from the console. A program that called `open` once
serves nobody from then on.

The `openForever` loop opens the next one for you, on a backoff table. Put the wiring in `on('open')`
so it runs for every session. The loop never gives up. A refusal that waiting cannot clear is
retried too, just more slowly, so a host that gets a new key comes back by itself.

[Unattended hosts](/docs/guides/unattended-hosts/) is the guide to running one. It covers
what a fallback page paints from `loop.health`, what says a host needs a person, and the settings
to change before a fleet ships.

`BitskiffHost` reads `BITSKIFF_SECRET_KEY` when you pass no `key`, which is why the block above has
no key line. An explicit `key` option wins over the environment and is never logged. With neither,
`open` refuses at the call site and nothing reaches the service.

## Secret keys

Create one in the [console](https://console.dev.bitskiff.com), beside the publishable key, or from the
command line. The CLI tarball is on the [downloads page](/docs/downloads/) too:

```sh
npm i -g https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz
bitskiff config set endpoint https://api.dev.bitskiff.com
echo "$BITSKIFF_ORG_KEY" | bitskiff login
bitskiff config set project proj_...
bitskiff keys create --type secret --preset host-only --label node-host
```

The CLI already talks to the api of the docs site it came from, and the `config set endpoint` line
pins that choice. It has no project until you set one, and `keys create` writes into the project it
resolves, so the project comes before it. The [CLI page](/docs/packages/cli/) has the
endpoint each environment answers on and the other ways to pass a project.

The `host-only` preset is `sessions:open` and nothing else, which is all a machine that only hosts
needs. A preset is only a starting list. Add permissions to the key later if you need them.

## Restarts

A Node host remembers its session. Restart the process and it resumes the one it was running, with
its clients still in it if the restart finished inside the host grace.

Set `hostResumeWindow` to cover a redeploy, as the block above does with `5m`. The default
is `10s`, which covers a page reload and not much else. Past the window, the session ends, the
clients are told the host went offline, and the next session has a new join code. It is a
[duration string](/docs/guides/limits/#durations), like every duration you write.

Two verbs end the loop, and which one a signal means is yours to decide.
Calling `loop.detach()` ends the loop and leaves the session running out its host grace, with its
token in place. That is what a redeploy wants: the restarted process resumes the same session on
the same join code.

Calling `loop.stop()` ends the session and forgets its token. That is what a
real shutdown wants, and the wrong verb for a redeploy. A program that calls it on every `SIGTERM`
puts a new join code on every host each time it ships.

```js
import { BitskiffHost } from '@bitskiff/host-node';
import { openForever } from '@bitskiff/host-node/unattended';

const bitskiff = new BitskiffHost();
const loop = openForever(bitskiff, { session: { hostResumeWindow: '5m' } });

process.on('SIGTERM', () => loop.detach());     // a rolling deploy
process.on('SIGINT', () => void loop.stop());   // a real stop
```

```ts
import { BitskiffHost } from '@bitskiff/host-node';
import { openForever, type UnattendedLoop } from '@bitskiff/host-node/unattended';

const bitskiff = new BitskiffHost();
const loop: UnattendedLoop = openForever(bitskiff, { session: { hostResumeWindow: '5m' } });

process.on('SIGTERM', () => loop.detach());     // a rolling deploy
process.on('SIGINT', () => void loop.stop());   // a real stop
```

The session is remembered in a sealed record, and a restart on Kubernetes needs that record on a
volume. [Server hosts](/docs/guides/server-hosts/) has both.

## Several hosts on one machine

Give each process its own name, so each remembers its own session:

```js
import { BitskiffHost } from '@bitskiff/host-node';
import { openForever } from '@bitskiff/host-node/unattended';

const bitskiff = new BitskiffHost();
const loop = openForever(bitskiff, { session: { persist: { as: 'front-desk-host' } } });

loop.on('open', (session) => console.log(session.joinCode.code));
```

```ts
import { BitskiffHost, type Session } from '@bitskiff/host-node';
import { openForever } from '@bitskiff/host-node/unattended';

const bitskiff = new BitskiffHost();
const loop = openForever(bitskiff, { session: { persist: { as: 'front-desk-host' } } });

loop.on('open', (session: Session) => console.log(session.joinCode.code));
```

Pass `persist: { in: 'none' }` to turn the remembering off instead. Two processes cannot share one
name. The second `open` is refused rather than taking the first one's session. The loop tries
again in a second or two, which is what a restart that overlaps the process it replaces needs.

## Peer-to-peer connections

A Node host turns on [`peer`](/docs/guides/transports/) with a peer connection of its own. The SDK
is tested against werift. Install it beside this package:

```sh
npm install werift
```

Import `RTCPeerConnection` from it, then pass the peer connection in `peer({ peerConnection })`.
Declare it on the host before the loop starts, so every session the loop opens has it:

```js
import { BitskiffHost, peer } from '@bitskiff/host-node';
import { openForever } from '@bitskiff/host-node/unattended';
import { RTCPeerConnection } from 'werift';

const bitskiff = new BitskiffHost();
bitskiff.transport(peer({ peerConnection: (config) => new RTCPeerConnection(config) }));
const loop = openForever(bitskiff);
```

```ts
import { BitskiffHost, peer } from '@bitskiff/host-node';
import { openForever, type UnattendedLoop } from '@bitskiff/host-node/unattended';
import { RTCPeerConnection } from 'werift';

const bitskiff = new BitskiffHost();
bitskiff.transport(peer({ peerConnection: (config) => new RTCPeerConnection(config) }));
const loop: UnattendedLoop = openForever(bitskiff);
```

Without `peerConnection`, every client's `peer` reads `unavailable` with reason `notAttached`.

## Next steps

- [Unattended hosts](/docs/guides/unattended-hosts/)
- [Channels](/docs/guides/channels/)
- [Lifecycle](/docs/guides/lifecycle/)
- [Client pages](/docs/guides/client-page/)
