# Hosting your own Orb client, and your own replica

October 4, 2026. Three separate things are often confused, so they are separated
here on purpose:

| You want | You need | Orb's server? |
| --- | --- | --- |
| Read an archive you are entitled to | a client, an RPC, a reachable block provider, your recovery secret | **No** |
| Keep copies of blocks for others | a block host and disk | **No** |
| Continue a live community | synchronization or federation between servers | **Yes — not solved here** |

## 1. Host the client only

This is the independence case. A client is static files; it holds no key, no
database and no session.

```sh
npm run independent:build          # rebuilds public/independent and its ZIP
cd public/independent
node verify-client.mjs             # every file against bundle-manifest.json
node serve.mjs                     # loopback preview on 127.0.0.1:8080
```

Or serve the extracted `orb-independent.zip` from any HTTPS static host at any
path. Requirements: correct JavaScript and WASM MIME types, worker support, no
`file://`. The bundle resolves every asset relative to itself
(`configureIndependentAssets` in `lib/independent-assets.ts`), so there is no
hardcoded Orb host. Checksums detect changes against a copy you already trust;
they do not identify the publisher, so compare against a release you obtained
deliberately.

Then open the client, choose an HTTPS RPC for chain 10143, connect the wallet you
control — an external browser wallet works; Privy is only one way to hold the
embedded wallet — and recover. `scripts/test-independence.mjs` is the executable
form of this claim: it recovers with **no Orb API, no database, no cookie, no
Privy, no browser storage and no signature**, and asserts that no request to the
original API was made.

The client needs three things it does not contain:

- **An RPC** that serves chain 10143 state for the index and identity contracts.
  The reader trusts that RPC; these are not cryptographic state proofs.
- **At least one block provider** that still holds the ciphertext. The gateways
  come from `chat-index.deployment.json` and can be overridden in the panel.
- **Your recovery secret** — a passphrase, or a wallet that still signs the
  recovery message identically. See
  [the protocol](orb-archive-protocol.md#4-recovery-identity-and-factors) for why
  the passphrase route exists.

## 2. Host a replica for others

Encrypted blocks are content-addressed and opaque. A replica operator cannot read
them, cannot tell whose they are, and gains nothing by altering them: every reader
re-hashes against the digest anchored onchain, and a mismatch is discarded.

The reference host is `infra/chat-archive/block-host.mjs` — no dependencies:

```sh
ORB_BLOCK_HOST_ROOT=/srv/orb-blocks \
ORB_BLOCK_HOST_TOKEN=$(openssl rand -hex 32) \
ORB_BLOCK_HOST_PORT=8093 ORB_BLOCK_HOST_HOST=127.0.0.1 \
node infra/chat-archive/block-host.mjs
```

Contract: `PUT /<64-hex-digest>` with raw bytes and `Authorization: Bearer <token>`,
`GET /<64-hex-digest>` for the bytes back. It refuses bytes whose SHA-256 is not
the address they were sent to, which is what stops one client poisoning a digest
another client will later verify. Reads are public by default because ciphertext
at a digest is not a secret; set `ORB_BLOCK_HOST_READ_TOKEN` to require a bearer
for reads too. Serve it behind HTTPS in production and put a CDN in front if you
want it to scale.

Point an Orb server at it with `ORB_CHAT_PROVIDERS`:

```dotenv
ORB_CHAT_PROVIDERS=[{"id":"offsite-b","kind":"http","url":"https://blocks.example/","authorization":"Bearer SERVER_ONLY_SECRET"}]
ORB_CHAT_STORAGE_QUORUM=2
```

Kinds are `folder`, `kubo` and `http`; adding a replica is configuration, not a
code change. Nothing is reported as durable until the configured quorum has been
written **and read back and re-hashed**. Below quorum the write fails, which is
the correct outcome: an unreplicated backup is not a backup. The same write floor
applies to every self-hosted replica folder (`ORB_CHAT_MIN_FREE_BYTES`, 2 GiB
default), because a host where web, proxy and media share one filesystem fails
everything at once when the disk fills.

For a full operator deployment — the web service, the reference keeper, sharding,
gas wallets, journals, repair and monitoring — see
`infra/chat-archive/README.md`. It is not installed on Oracle by this change.

Two honest warnings. A CID proves identity, not retention: a provider that stops
serving a block makes it unrecoverable and no key can recreate it. And two folders
on one computer, or two processes on one host, are **not** two failure domains;
`scripts/test-independence.mjs` runs two separate host processes to exercise the
mechanism, and says so rather than claiming offsite replication it did not do.

## 3. What is deliberately not solved: live continuity

Everything above recovers an **archive**: a record of what was said, readable by
the wallets that were authorized when it was written. It is not a running
community.

A live community needs members to see each other's new messages without a shared
hosted database. That requires either synchronization between servers that agree
on ordering, or federation with a common delivery layer. Neither exists here, and
neither is implied by anything above:

- Live memberships, roles, moderation, bans, invites and ownership stay in the
  hosted database. An archive does not carry them and cannot restore them.
- A recovered copy is a personal historical record. It is not proof that someone
  else wrote the text in it; only a newly published, signed envelope authenticates
  its own publisher.
- Attachments, media, voice and video are not archived at all.
- A former recipient cannot be made to forget what was already decrypted for them.
  There is no MLS and no Double Ratchet, so there is no forward secrecy after a
  long-term key is compromised.
- Channel blueprints in a community capsule are public descriptive templates. They
  are not enforced permission rules and not a restored chat server.

The XMTP inbox in the client is a separate experiment on a separate dev network,
with its own identity signing and an unencrypted browser database. It is not a
migration path for hosted channels, and production XMTP needs gateway and fee
configuration.

So: **an exit path for a community's record, yes. A serverless live community,
no.** Claiming the second from the first would be the easiest way to make this
document a lie.

## 3. Put your own copy on GitHub

The client is static files, so "my own Orb, on my own account" is a repository and
a hosting setting, not a server.

1. Fork the repository (or push a copy to an account you control). The build that
   matters for a client is `npm run independent:build`; it writes
   `public/independent/` and `orb-independent.zip`.
2. Publish it one of two ways:
   - **GitHub Pages**: point Pages at the folder (or at a `gh-pages` branch you
     copy `public/independent/` into). Pages serves over HTTPS and preserves the
     WASM and worker MIME types the bundle needs.
   - **A release**: attach `orb-independent.zip` to a GitHub release. Readers then
     obtain your client from *your* tag, which is the only way a checksum
     comparison against you means anything.
3. Verify before you trust, and say so in your README:
   `node verify-client.mjs` checks every file in the folder against
   `bundle-manifest.json`. Publish the manifest digest next to the release so a
   reader can compare what they downloaded with what you built.
4. Open it, choose an HTTPS RPC for chain 10143, connect a wallet you control and
   recover. No Orb account, no Orb API, no cookie.

What you are vouching for when you host a client: the code that decrypts a
reader's archive in their browser. A checksum tells a reader you shipped what you
say you shipped; it does not tell them the code is honest. That is why the
recovery path is also executable as a test -
`scripts/test-independence.mjs` - and why the reader can run it against your fork
before trusting it.

## 4. Run the web service yourself

The hosted service is a Next.js app plus SQLite-compatible LibSQL and, optionally,
the archive keeper. For a single box:

```sh
cp .env.example .env            # fill what you have; leave the rest empty
npm ci
node scripts/migrate.mjs        # TURSO_DATABASE_URL must point at your file: database
npm run build && npm start      # or `npm run dev` while developing
```

Behind Caddy (see `infra/orb-site.caddy`), set `ORBITA_TRUST_LOCAL_PROXY=1`. The
proxy is what tells the server who the caller is; without it a production server
cannot separate two anonymous callers and therefore refuses anonymous actions
instead of letting everyone share one rate-limit bucket. Local development on
loopback needs neither the proxy nor the variable.

Everything that costs gas - the attestation keeper, the archive index keeper, the
arena relay - runs as separate processes with their own gas-only wallets and is
documented in `infra/chat-archive/README.md` and `docs/orb-attestation-passport.md`.
None of them ever holds a member key.
