# Using `@decentnetwork/peer` in a Node.js application

A practical, copy-paste-ready guide for building Node.js apps on top
of `@decentnetwork/peer`. Every code block is a complete runnable file —
copy it, save it as `.mjs` or `.ts`, install the dep, and run.

If you haven't read the protocol docs yet, this guide stands on its
own. For deeper reference once you have a working app:

- [`PROTOCOL_OVERVIEW.md`](PROTOCOL_OVERVIEW.md) — actors, lifecycle,
  message kinds
- [`DISCOVERY_FLOW.md`](DISCOVERY_FLOW.md) — what happens between
  `peer.start()` and "friend shows online"
- [`IOS_INTEROP_PLAYBOOK.md`](IOS_INTEROP_PLAYBOOK.md) — debugging
  guide when something goes wrong

---

## Install

Requires Node.js 20 or later (we use modern WebStreams/AbortController).

```bash
pnpm add @decentnetwork/peer
# or
npm install @decentnetwork/peer
# or
yarn add @decentnetwork/peer
```

There are **no** native dependencies. Pure JS via `tweetnacl` for
NaCl crypto and `flatbuffers` for app payloads.

---

## Concepts in 60 seconds

1. **A peer has a long-term keypair** (loaded from / saved to a file
   on disk). The pubkey is your identity. Sharing your **address**
   (`pubkey + nospam + checksum`, base58-encoded) lets anyone send
   you a friend request.
2. **Friends are pubkeys you've added**. After both sides have
   added each other, an encrypted session establishes automatically
   in the background. `peer.onText(...)` fires when they message you.
3. **Bootstrap nodes are operator-run servers** that introduce you
   to the DHT and act as TCP relays when direct UDP isn't possible.
   You don't run them — you list them in your config.
4. **The protocol handles offline delivery**: friend requests and
   text messages can ride an HTTP "Express" relay so they arrive
   when the recipient next comes online.

That's it. The rest is event handlers and lifecycle.

---

## Sample 1 — Hello world: start, print address, stop

The simplest possible peer: starts, prints your address (so you can
give it to a friend), waits 5 seconds, stops cleanly.

```js
// hello.mjs
//
// Run with:
//   node hello.mjs
//
// On first run this creates `peer.save` (your keypair). Keep that
// file safe — it IS your peer identity. Delete it and you become a
// new peer with no friends and no message history.

import { Peer } from "@decentnetwork/peer";

// `Peer.create(...)` only constructs the object. Network IO is
// deferred to `start()` so you can attach event listeners first
// (see Sample 2).
const peer = await Peer.create({
  // Where to load/save the long-term keypair. Created on first run
  // if not present.
  keyFile: "./peer.save",

  // Bootstrap nodes — these are operator-run servers that introduce
  // us to the DHT. The list is synced with iOS Beagle's hardcoded
  // bootstraps. See `legacy-bootstraps.mjs` for the full list; one
  // node here is enough to bootstrap, but more = better resilience.
  bootstrapNodes: [
    {
      host: "47.100.103.201",
      port: 33445,
      pk: "CX1XH419p4xJ5SV4KvDxBeKYSRdMJW9QpdWJY8owUxHd"
    },
    {
      host: "144.202.113.167",
      port: 33445,
      pk: "EfT4YMq6qfHdDsCiBCgsEmA78E2NxVYVKVUS9bD6w9GH"
    },
    {
      host: "13.58.208.50",
      port: 33445,
      pk: "89vny8MrKdDKs7Uta9RdVmspPjnRMdwMmaiEW27pZ7gh"
    }
  ],

  // "legacy" = wire-compatible with iOS Beagle / Carrier C SDK.
  // Currently the only supported mode.
  compatibilityMode: "legacy"
});

// Bind UDP socket, load persisted friends.json (if any).
await peer.start();

// Send a `getnodes` to each bootstrap; whichever responds first
// becomes our entry into the DHT. Returns once we've joined.
const result = await peer.joinNetwork();
console.log(`joined via ${result.respondingNode.host}:${result.respondingNode.port}`);
console.log(`discovered ${result.discoveredNodes.length} neighbors`);

// `address` = pubkey + nospam + checksum, base58. Share this with
// friends so they can `sendFriendRequest` you.
console.log(`my address: ${peer.address()}`);

// `userid` = just the 32-byte pubkey, base58. Used internally for
// friend lookups; you generally pass the address, not the userid.
console.log(`my userid:  ${peer.userid()}`);

// Idle for 5 seconds so the DHT-PK / self-announce loops can do a
// few rounds. In a real app you'd run forever.
await new Promise((r) => setTimeout(r, 5_000));

// Tear down cleanly: closes UDP socket, closes TCP relay
// connections, persists friends.json.
await peer.stop();
console.log("stopped.");
```

---

## Sample 2 — Receive friend requests and accept

Run this on machine A; share the printed address; have machine B
call `peer.sendFriendRequest(addr, "hi")` (Sample 3 below).

```js
// receiver.mjs

import { Peer } from "@decentnetwork/peer";
import { defaultBootstrapNodes } from "@decentnetwork/peer/scripts/legacy-bootstraps.mjs";
//   ^ for production code, just inline a few bootstraps from the README

const peer = await Peer.create({
  keyFile: "./receiver.save",
  bootstrapNodes: defaultBootstrapNodes,
  compatibilityMode: "legacy"
});

// Attach event listeners BEFORE start() so we don't miss anything
// that happens early (e.g. a friend already tried to reach us
// before we could bind the listener).

peer.onFriendRequest((request) => {
  // request fields:
  //   pubkey      — friend's pubkey (= userid)
  //   userid      — same as pubkey (alias)
  //   address     — friend's full address (when known)
  //   nospam      — friend's nospam u32 (when known)
  //   name        — display name in their friend-request payload
  //   description — status message in their payload
  //   hello       — the optional greeting they included
  console.log(`friend request from ${request.userid}`);
  console.log(`  name: ${request.name}`);
  console.log(`  description: ${request.description}`);
  console.log(`  hello: ${request.hello}`);

  // Auto-accept everyone for this demo. In a real app you'd queue
  // these for user approval.
  void peer.acceptFriendRequest(request.pubkey).then(() => {
    console.log(`accepted ${request.userid}`);
  });
});

peer.onFriendConnection((event) => {
  // Fires when a friend's online/offline state changes. event:
  //   pubkey — friend
  //   status — "connected" | "disconnected"
  console.log(`${event.pubkey} is now ${event.status}`);
});

peer.onFriendInfo((event) => {
  // Fires when we learn a friend's display name / status message.
  console.log(`${event.pubkey} updated: name="${event.name}" desc="${event.description}"`);
});

peer.onText((msg) => {
  // Inbound text message: msg.pubkey, msg.text.
  console.log(`<${msg.pubkey}> ${msg.text}`);

  // Echo back. `sendText` returns a Promise; resolves when the
  // message is queued via TCP relay or Express. May throw if the
  // friend isn't a friend (we never accepted/added them).
  void peer.sendText(msg.pubkey, `echo: ${msg.text}`)
    .catch((err) => console.error(`send failed: ${err.message}`));
});

await peer.start();
await peer.joinNetwork();
console.log(`address (give this to senders): ${peer.address()}`);

// Run forever — Ctrl+C to stop. In a long-running daemon, register
// a SIGINT handler that calls peer.stop() so friends.json gets
// flushed cleanly.
process.on("SIGINT", async () => {
  console.log("\nstopping...");
  await peer.stop();
  process.exit(0);
});
```

---

## Sample 3 — Send a friend request and a message

```js
// sender.mjs

import { Peer } from "@decentnetwork/peer";

// Paste the address printed by receiver.mjs:
const FRIEND_ADDRESS = "<paste address here>";

const peer = await Peer.create({
  keyFile: "./sender.save",
  bootstrapNodes: [
    { host: "47.100.103.201", port: 33445, pk: "CX1XH419p4xJ5SV4KvDxBeKYSRdMJW9QpdWJY8owUxHd" },
    { host: "154.64.235.176", port: 33445, pk: "GdNtV2N74fZnLjhH7NhQ18nGdxb1k8jRM9dQaK7WnxmL" }
  ],
  compatibilityMode: "legacy"
});

// Watch for the receiver's confirmation.
peer.onFriendConnection((event) => {
  if (event.status === "connected") {
    console.log(`✓ session up with ${event.pubkey}`);
  }
});
peer.onText((msg) => {
  console.log(`<reply from ${msg.pubkey}> ${msg.text}`);
});

await peer.start();
await peer.joinNetwork();

// `sendFriendRequest`:
//   - validates the address (parses pubkey + nospam + checksum)
//   - adds the friend to our list with status="requested"
//   - sends the request via onion routing AND posts to the express
//     offline relay in parallel (whichever delivers first wins)
//
// Returns once both dispatches are queued. The receiver's
// `onFriendRequest` event fires on their side; once they call
// `acceptFriendRequest` and a session establishes, we'll see a
// `friendConnection { status: "connected" }` event.
console.log(`sending friend request to ${FRIEND_ADDRESS}...`);
await peer.sendFriendRequest(FRIEND_ADDRESS, "hi from sender.mjs");

console.log(`waiting for accept...`);

// Wait up to 60s for the session to come up. waitForFriendConnected
// extracts the userid from the address and resolves true on the
// first "connected" event.
import { carrierIdFromAddress } from "@decentnetwork/peer";
const friendUserId = carrierIdFromAddress(FRIEND_ADDRESS);
const ok = await peer.waitForFriendConnected(friendUserId, 60_000);
if (!ok) {
  console.log("timed out waiting for accept; try again later");
  await peer.stop();
  process.exit(1);
}

console.log(`✓ sending text...`);
await peer.sendText(friendUserId, "first message");
await peer.sendText(friendUserId, "second message");

// Give the receiver a few seconds to reply before we shut down.
await new Promise((r) => setTimeout(r, 8_000));

await peer.stop();
```

---

## Sample 4 — Echo bot (long-running daemon)

A reusable headless bot that:
- accepts every incoming friend request
- echoes back whatever is sent to it
- handles graceful shutdown
- logs to a file

```js
// echobot.mjs
//
// A long-running bot. Run with:
//   node echobot.mjs &
//
// Logs go to ./echobot.log. Stop with `kill <pid>`.

import { Peer } from "@decentnetwork/peer";
import { appendFile } from "node:fs/promises";

const LOG_FILE = "./echobot.log";

async function log(line) {
  const stamp = new Date().toISOString();
  await appendFile(LOG_FILE, `${stamp} ${line}\n`);
  console.log(line);
}

const peer = await Peer.create({
  keyFile: "./echobot.save",
  bootstrapNodes: [
    { host: "47.100.103.201", port: 33445, pk: "CX1XH419p4xJ5SV4KvDxBeKYSRdMJW9QpdWJY8owUxHd" },
    { host: "154.64.235.176", port: 33445, pk: "GdNtV2N74fZnLjhH7NhQ18nGdxb1k8jRM9dQaK7WnxmL" },
    { host: "45.207.220.155", port: 33445, pk: "37PXijTXkyX9rUcRFjGo7PYpwf732KXQJgMtcKuMu3ym" }
  ],
  // Optional debug label that shows in logs (handy when you run
  // multiple peers in the same process):
  debugLabel: "echobot",
  compatibilityMode: "legacy"
});

peer.onFriendRequest(async (req) => {
  await log(`friend request from ${req.userid} ("${req.hello}"); accepting`);
  await peer.acceptFriendRequest(req.pubkey).catch((err) =>
    log(`accept failed: ${err.message}`)
  );
});

peer.onFriendConnection(async (e) => {
  await log(`${e.pubkey} -> ${e.status}`);
});

peer.onText(async (msg) => {
  await log(`<${msg.pubkey}> ${msg.text}`);
  // Sleep 200ms before replying so iOS UI has time to render the
  // incoming bubble before the auto-reply lands.
  await new Promise((r) => setTimeout(r, 200));
  try {
    await peer.sendText(msg.pubkey, `echo: ${msg.text}`);
    await log(`replied to ${msg.pubkey}`);
  } catch (err) {
    await log(`reply failed: ${err.message}`);
  }
});

await peer.start();
await peer.joinNetwork();
await log(`started; address=${peer.address()}`);

// Graceful shutdown so friends.json is flushed and TCP relays
// closed cleanly (otherwise relays keep our slot for a while).
const shutdown = async (sig) => {
  await log(`received ${sig}; stopping`);
  await peer.stop();
  process.exit(0);
};
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));

// Keep the process alive forever.
await new Promise(() => {});
```

---

## Sample 5 — Interactive CLI chat

A two-pane REPL where you type messages to a single friend and see
their replies inline. Uses Node's built-in `readline`.

```js
// chat.mjs
//
// Usage:
//   node chat.mjs <friend-address>     # connect to friend, then type
//
// Or omit the argument to just run as a receiver and let someone
// add you (we print our own address at startup).

import { Peer, carrierIdFromAddress } from "@decentnetwork/peer";
import { createInterface } from "node:readline/promises";
import { stdin, stdout } from "node:process";

const FRIEND_ADDRESS = process.argv[2];

const peer = await Peer.create({
  keyFile: "./chat.save",
  bootstrapNodes: [
    { host: "47.100.103.201", port: 33445, pk: "CX1XH419p4xJ5SV4KvDxBeKYSRdMJW9QpdWJY8owUxHd" },
    { host: "154.64.235.176", port: 33445, pk: "GdNtV2N74fZnLjhH7NhQ18nGdxb1k8jRM9dQaK7WnxmL" }
  ],
  compatibilityMode: "legacy"
});

let activeFriend; // friend userid we're chatting with

peer.onFriendRequest(async (req) => {
  console.log(`\n[friend request from ${req.userid}: "${req.hello}"]`);
  console.log(`accepting automatically.`);
  await peer.acceptFriendRequest(req.pubkey);
  if (!activeFriend) {
    activeFriend = req.pubkey;
    console.log(`active chat: ${activeFriend}`);
  }
});

peer.onFriendConnection((e) => {
  console.log(`\n[${e.pubkey} ${e.status}]`);
});

peer.onText((msg) => {
  // The "\r\x1b[K" clears the current line so the inbound bubble
  // doesn't collide with the readline prompt the user is typing on.
  process.stdout.write(`\r\x1b[K<${msg.pubkey.slice(0, 8)}…> ${msg.text}\n`);
  process.stdout.write("> ");
});

await peer.start();
await peer.joinNetwork();
console.log(`my address: ${peer.address()}`);

if (FRIEND_ADDRESS) {
  console.log(`adding friend ${FRIEND_ADDRESS}`);
  await peer.sendFriendRequest(FRIEND_ADDRESS, "let's chat");
  activeFriend = carrierIdFromAddress(FRIEND_ADDRESS);
  console.log(`waiting for them to accept...`);
  const ok = await peer.waitForFriendConnected(activeFriend, 60_000);
  if (!ok) {
    console.log(`timed out — they may accept later`);
  }
}

const rl = createInterface({ input: stdin, output: stdout });
console.log(`type messages, blank line to quit:`);
while (true) {
  const line = (await rl.question("> ")).trim();
  if (line === "") break;
  if (!activeFriend) {
    console.log(`no active friend yet — wait for someone to add you, or pass <address> on the command line`);
    continue;
  }
  try {
    await peer.sendText(activeFriend, line);
  } catch (err) {
    console.error(`send failed: ${err.message}`);
  }
}

rl.close();
await peer.stop();
```

---

## Sample 6 — Custom bootstrap nodes

For private deployments you can run your own bootstrap server
(`bootstrapd` from the Carrier source) and point peers at it:

```js
import { Peer } from "@decentnetwork/peer";

const peer = await Peer.create({
  keyFile: "./peer.save",
  bootstrapNodes: [
    // Your private bootstrap (no public DHT participation):
    {
      host: "bootstrap.example.com",
      port: 33445,
      pk: "<base58 pubkey of your bootstrap>"
    }
    // Optionally include the public ones too for redundancy:
    // { host: "47.100.103.201", port: 33445, pk: "CX1XH419..." }
  ],
  // For an air-gapped private network, disable Express to avoid
  // talking to lens.beagle.chat:
  expressNodes: [],
  compatibilityMode: "legacy"
});

await peer.start();
await peer.joinNetwork();
```

To deploy your own bootstrap, see the
[Carrier bootstrap source](https://github.com/elastos/Elastos.CarrierClassic.Bootstrap).
A single bootstrap on a $5/month VPS handles thousands of peers.

---

## Sample 7 — TypeScript with strict types

The package ships full `.d.ts`. Every exported symbol has a
TypeScript type:

```ts
// chat.ts

import {
  Peer,
  type PeerOptions,
  type FriendRequest,
  type FriendConnectionEvent,
  type TextMessage,
  carrierIdFromAddress
} from "@decentnetwork/peer";

const opts: PeerOptions = {
  keyFile: "./peer.save",
  bootstrapNodes: [
    { host: "47.100.103.201", port: 33445, pk: "CX1XH419p4xJ5SV4KvDxBeKYSRdMJW9QpdWJY8owUxHd" }
  ],
  compatibilityMode: "legacy"
};

const peer = await Peer.create(opts);

peer.onFriendRequest((req: FriendRequest) => {
  console.log(req.userid, req.hello);
});

peer.onFriendConnection((ev: FriendConnectionEvent) => {
  console.log(ev.pubkey, ev.status);
});

peer.onText((msg: TextMessage) => {
  console.log(msg.pubkey, msg.text);
});

await peer.start();
await peer.joinNetwork();
console.log(peer.address());
```

---

## Public API reference

### `Peer.create(opts: PeerOptions): Promise<Peer>`

Constructs a peer. Doesn't open any sockets — call `start()` next.

`PeerOptions`:

| Field | Type | Required | Notes |
|---|---|---|---|
| `keyFile` | `string` | yes | Path to the long-term keypair file (auto-created on first run) |
| `bootstrapNodes` | `NetworkNode[]` | yes | At least one DHT bootstrap; more = better resilience |
| `expressNodes` | `NetworkNode[]` | no | HTTP offline relay (default: `lens.beagle.chat:443`) |
| `compatibilityMode` | `"legacy"` | no | Only "legacy" supported. Defaults to legacy. |
| `friendStoreFile` | `string` | no | Defaults to `<keyFile>.friends.json` |
| `debugLabel` | `string` | no | Prepended to debug log lines |

### Lifecycle

- `peer.start(): Promise<void>` — bind UDP, open TCP relays, load friends
- `peer.stop(): Promise<void>` — flush friends, close sockets/relays
- `peer.joinNetwork(): Promise<BootstrapResult>` — DHT bootstrap

### Identity

- `peer.pubkey(): string` — 32-byte pubkey, base58 — same as `userid()`
- `peer.userid(): string` — same as `pubkey()`
- `peer.address(): string` — pubkey + nospam + checksum, base58 — share this

### Friends (data plane)

- `peer.sendFriendRequest(address: string, hello?: string): Promise<void>`
- `peer.acceptFriendRequest(pubkey: string): Promise<void>`
- `peer.rejectFriendRequest(pubkey: string): void`
- `peer.removeFriend(addressOrUserid: string): boolean` — drops a friend, returns true if existed
- `peer.sendText(pubkey: string, text: string): Promise<void>`
- `peer.friends(): FriendRecord[]` — current friends list
- `peer.waitForFriendRequest(timeoutMs?: number): Promise<FriendRequest>`
- `peer.waitForFriendConnected(pubkey: string, timeoutMs?: number): Promise<boolean>`

### Events

| Method | Event payload |
|---|---|
| `peer.onFriendRequest(cb)` | `FriendRequest` — incoming pending request |
| `peer.onFriendConnection(cb)` | `FriendConnectionEvent` — `{pubkey, status}` |
| `peer.onFriendInfo(cb)` | `FriendInfoEvent` — `{pubkey, userid, name, description}` |
| `peer.onText(cb)` | `TextMessage` — `{pubkey, text}` |

### Helpers (re-exported from `compat/address.ts`)

- `carrierAddressFromPublicKey(publicKey, nospam?): string`
- `carrierIdFromAddress(address): string` — strip nospam+checksum
- `carrierIdFromPublicKey(publicKey): string`
- `parseCarrierAddress(address): CarrierAddressParts`

### Constants

- `CARRIER_PUBLIC_KEY_SIZE = 32`
- `CARRIER_ADDRESS_SIZE = 38`

---

## Configuration via environment

All operator-tuneable knobs are listed in the main README under
"Configuration". Most users won't need any of them — defaults match
toxcore / iOS Beagle.

The most useful ones during development:

```bash
# Enable debug logs
DECENT_DEBUG=1 node app.mjs

# Disable Express (offline relay) for cleaner test traces
DECENT_DISABLE_EXPRESS=1 node app.mjs

# Auto-accept incoming friend requests (useful for bots/tests)
DECENT_AUTO_ACCEPT=1 node app.mjs
```

---

## Common patterns

### Wait until a friend's session is established

```js
// Returns true once the friend's session is up, false on timeout.
const userid = carrierIdFromAddress(theirAddress);
const ok = await peer.waitForFriendConnected(userid, 60_000);
if (!ok) {
  console.error("friend didn't come online in 60s");
}
```

### Persist incoming messages to your own database

```js
import { open } from "node:sqlite"; // or any DB driver

const db = await open({ filename: "./messages.db" });
await db.run(`create table if not exists msg (id integer primary key, peer text, text text, ts integer)`);

peer.onText(async (msg) => {
  await db.run(`insert into msg(peer,text,ts) values (?,?,?)`, msg.pubkey, msg.text, Date.now());
});
```

### Multi-process: one Peer per process

`@decentnetwork/peer` binds a UDP socket; you can only have one peer per
keyfile per port. If you want multiple peer identities on the same
machine, spawn separate processes each with its own `keyFile`. Use
`debugLabel` to tag their logs.

### Custom UDP port

The transport tries 33445, 33446, 33447, … falling back to
ephemeral. To force a specific port, override the bootstrap setup
or set `DECENT_UDP_PORT_PREFER=N` (env-only currently).

---

## Error handling

All async methods reject with an `Error` (or subclass). Handle
them as you would any Promise rejection. The most common failure
modes:

| Error message | Likely cause |
|---|---|
| `Peer is not started` | `joinNetwork` / `pubkey` called before `start` |
| `bootstrap timed out after 30000ms` | All bootstraps unreachable; check `bootstrapNodes` |
| `Carrier address checksum mismatch` | The `address` string passed to `sendFriendRequest` is malformed |
| `Not a friend: <userid>` | `sendText` called for someone you haven't added |
| `friend is offline and no express node is configured` | Receiver is offline, you disabled Express, no other path |
| `friend session unavailable for <userid>` | Internal — session was torn down between check and send |

For deeper diagnosis, run with `DECENT_DEBUG=1` and consult the
debug markers in [`IOS_INTEROP_PLAYBOOK.md`](IOS_INTEROP_PLAYBOOK.md) §5.

---

## What you can build

This package gives you a P2P encrypted messaging substrate. Apps
others have built / are building on it:

- Peer-to-peer chat clients (CLI, Electron, mobile)
- Decentralized notification systems (peer-to-peer push)
- Off-grid messaging companions for IoT/robotics
- Backend bots (echo, indexing, monitoring)
- File-transfer prototypes (chunk text messages, or roll your own
  on top of `peer.sendText` with framing)

Future capabilities the package doesn't yet offer (PRs welcome):

- Group chats / conferences
- Native file-transfer (`tox_file_send` equivalent)
- Voice/video (TURN/STUN integration)

---

## Getting help

- **Issues**: <https://github.com/0xli/peer/issues>
- **Bug catalog from real iOS interop testing**: [`IOS_INTEROP_PLAYBOOK.md`](IOS_INTEROP_PLAYBOOK.md)
- **Protocol questions**: read [`PROTOCOL_OVERVIEW.md`](PROTOCOL_OVERVIEW.md)
  and [`CARRIER_VS_TOX.md`](CARRIER_VS_TOX.md) — most "why doesn't X
  work?" questions are answered by the protocol semantics

When opening an issue, please include:

1. The `Peer.create(...)` options (with the keyFile path
   redacted)
2. A `DECENT_DEBUG=1` log of the failure, with the relevant
   ~30 lines around the unexpected behavior
3. Your Node version and OS
