# Portable chat and encrypted archives on Monad

**October 4 update:** the preferred local backup path now uses the
[scalable encrypted archive](orb-chat-scale.md): external replicated ciphertext,
small indexed Monad references, bounded/resumable exports and paged recovery.
The full-ciphertext protocol below remains available as a separate legacy pilot.
The same registered wallet recovery identity is reused.

October 3, 2026. **Testnet prototype, chain 10143.** The archive contract and a
two-wallet rehearsal are confirmed on public testnet. The new application UI,
queue, static client and keeper integration are local changes; this iteration has
not pushed Git or deployed a new Oracle web release.

## What a member can do

Open `/independent?tab=chat`, using the original Orb Wallet. Enable/recover the
chat key, then choose **Backup**, **Recovered history** or **Onchain chat**.
Community profiles link this panel under their portability controls.

1. The wallet signs a private recovery document on the device. A random P-256 chat
   key is sealed with HKDF-SHA-256 / AES-256-GCM. The chain stores its public key
   and sealed backup, **never the recovery signature or unencrypted private key**.
   Creation checks that two identical requests return the same signature. A
   recovered key must match the registered public key; failure never rotates it.
2. With explicit consent, the client copies accessible text and community/channel
   names, decrypting existing readable E2EE locally and encrypting each archive
   part for its own wallet. Hosted source access requires the Orb session, the
   profile-bound Privy wallet and current channel/DM permissions. Bans apply.
3. Only ciphertext and bounded signatures reach the authenticated outbox. A
   registered, revocable chat-only session lasts at most 24 hours. Its private key
   remains in memory and cannot transfer funds, approve tokens or sign wallet
   recovery messages. Reloading requires another reviewed session registration.
4. Publish the pending parts yourself, or run the keeper with an operator gas
   wallet. A pending outbox is **not a confirmed chain backup**. Successful exact
   receipts are required. Expired signatures can be renewed without changing the
   ciphertext. Nonce conflicts preserve the pending copy and stop publication.
5. On another authorized client, reconnect the **same wallet**, sign the same
   private recovery document, recover the non-extractable chat key locally, and
   read/decrypt the confirmed archive through Monad RPC. The independent reader
   does not call the Orb API or need its account database.

The six-hour option prepares incremental text archives only while this panel is
mounted, visible, unlocked and its chat session is active. It is opt-in and does
not make snapshots while the browser is closed. A keeper can publish already
signed parts while the browser is closed, until their signatures expire. Current
limits are 500 pending parts per account, 5,000 plaintext bytes per part, eight
entries / 12,000 total ciphertext bytes per transaction. Gas still costs test MON;
batching is not free storage and is not a measured TPS benchmark.

## New independent messages

The separate portable inbox publishes full ciphertext in `Archived` events.
Wallet DMs contain individual encrypted copies for sender and recipient. Both
wallets must have registered chat keys. The independent community's encrypted
general channel wraps the message key for every current eligible member; the
contract checks the sender's current registry access. The pilot supports 20
members and fails rather than sending plaintext when a member has no key.

Each entry is bound to the chain, contract, author, nonce, kind, recipient and
channel context. EIP-712 signatures authorize publication, and sequential nonces
prevent replay. Linked author/inbox/community block heads let a reader request
individual relevant blocks, respecting the public RPC's event-range limit. The
reader currently limits each linked stream to 5,000 blocks and a read to 16 MiB
of ciphertext. Larger archives need a paged reader; malformed personal parts are
excluded and counted. Personal restoration does not scan incoming DM traffic.
It needs an RPC that
retains historical logs and trusts that RPC; it is not a light client.

Known publication hashes persist before waiting for confirmation. An uncertain
receipt blocks another send until the exact pending receipt is checked. A lost
provider response before a transaction hash is returned remains an external
wallet/RPC failure case: inspect wallet activity rather than assuming no send.

## Scale boundary and recommended next architecture

4,000 communities with 30,000 messages each means **120 million messages**.
At an illustrative average of 500 UTF-8 bytes per message, text alone would be
60 GB (decimal), before metadata, encryption, attachments, indexes and personal
copies. This is an arithmetic example, not a measured workload or gas estimate.

The current full-ciphertext-onchain implementation is a **testnet pilot**. It
does not archive every community globally: the source checks the current user's
access, reads indexed 100-message pages and keeps incremental per-stream cursors.
Its 500-part pending queue, transaction bounds, gas budget, 20-member independent
group bound and 16 MiB reader limit deliberately stop oversized work. They do
not establish production capacity for the example above. The six-hour option
only prepares new content while the authorized, unlocked panel is active; it is
not an always-on global backup service. Long histories need resumable pagination
and recovery progress before they can be presented as complete archives.

For this scale, the recommended next design is encrypted incremental batches in
replicated content-addressed storage, with hashes/CIDs, stream indexes and
authorized key recovery anchored on Monad. A community batch should be stored
once for an authorized membership epoch, instead of copying the entire history
for each member. Keys and membership changes need versioned access rules; former
recipients cannot be made to forget content they already decrypted. Clients
should fetch only the chosen stream and page, verify its committed hash, then
decrypt locally with the recovered wallet-bound keys.

This hybrid design is now implemented separately in `OrbChatIndex` and the local
scalable archive panel; see [its scope and remaining gates](orb-chat-scale.md).
The old full-onchain pilot described here is not its production capacity test.
A CID alone is not a
retention guarantee: pinning/replication, retrieval failure handling and recovery
drills are required. Network TPS is not an unlimited or free storage allowance.
Capacity, gas and recovery time must be measured on realistic fixtures before
a production claim.

## What is and is not recovered

Archived community names, channel lists and message copies are **personal,
historical records**. They do not migrate live memberships, permissions, roles,
moderation, invites or ownership from the hosted database into the independent
registry. A member's archive is not cryptographic proof that someone else wrote
its copied text. New onchain messages have their own authenticated author record.

Attachments, images/GIF files, voice and video are not copied. Legacy text that
the current browser cannot decrypt is excluded and counted; this feature cannot
recreate a lost legacy key. Original hosted chat keys and their history are kept
unchanged. The portable identity is a separate, contract-bound identity.

Ciphertext onchain is permanent. Addresses, recipients, community IDs, timing,
sizes and traffic are public even though plaintext is encrypted. Former recipients
retain access to messages already encrypted for them. There is no Double Ratchet
or MLS, no promised forward secrecy after long-term-key compromise and no
independent cryptographic audit. A compromised device/client or someone who can
make the wallet sign the private recovery document can read its archive. Wallet
login, recovery and MFA are respected.

## Privy and hosting

The downloadable client offers lazy **Recover Orb Wallet with Privy** using the
original public app/client IDs. It never packages an app secret. Configure its
origin in the original Privy application's allowed origins/client settings. An
unrelated Privy app or the same email at another app does not provide the same
embedded wallet. Arbitrary third-party clients do not receive automatic access;
Privy authentication and signing still depend on Privy. Global-wallet support is
not configured by this feature. The same exported wallet can alternatively be
connected through a browser wallet. Use only a client you trust.

Download `/independent/orb-independent.zip`, verify it with
`node verify-client.mjs`, and serve with `node serve.mjs`. All compiled lazy SDK
chunks are included in the public allowlist and integrity manifest. Localhost and
arbitrary HTTPS subfolders are supported. Download checksums cannot
protect a client if the script and the expected checksum are both replaced.

## Operator setup

Apply migration `0046_orb_chat_outbox.sql` to the intended Web3 database. Create
an ignored `.env.chat.local` with its database URL/auth token, the archive RPC and
`ORB_CHAT_RELAYER_KEY` for an **operator gas wallet**, never a user's wallet or
recovery signature. `ORB_CHAT_DAILY_BUDGET_MON` defaults to 0.5 test MON. Do not
reuse this local preview database configuration for normal Órbita.

`npm run chat:keeper` polls once a minute; `npm run chat:keeper:once` runs one
cycle. The keeper verifies chain/runtime/registry, bounded personal archive
entries, author/session signatures, nonces, expiry, available balance and daily
budget. It journals the exact signed gas transaction atomically before broadcast,
resumes the same hash after interruption, checks receipt events, and prevents two
local keeper processes for the same deployment. Pending ciphertext can be read
and renewed from the same authenticated account on another device. Logs never
include recovery signatures or user private keys. Hosting a persistent keeper on
Oracle remains separate from the local setup; no Oracle service was installed by
this iteration.

## Verified evidence

Archive: [`0xdb4fa091B3D86f5c1835564Ed2E796AF0d2C5923`](https://testnet.monadscan.com/address/0xdb4fa091B3D86f5c1835564Ed2E796AF0d2C5923).
Deployment: [`0x4220fd…d7eac`](https://testnet.monadscan.com/tx/0x4220fd62ff60485be4c2ad49b8c32795178b91ba5a2ecca176c45ed54e9d7eac).
Confirmed personal archive: [`0x74742a…0caf`](https://testnet.monadscan.com/tx/0x74742a1acc7e84e3effad3c85b2940a9a92caedb801c0209340d71a66acc0caf).
Confirmed A→B DM: [`0x231485…d77f2`](https://testnet.monadscan.com/tx/0x2314853b5fec9f65894df553474f144008ad08395dce21172ecf0b9e694d77f2).

- `chat:test`: real local contract and browser cryptography; immutable keys, P-256
  validation, sealed recovery, foreign-wallet/context/tamper/replay rejection,
  session revocation and community access.
- `chat:test:api`: real cookies, database permissions and signatures, source and
  queue isolation, CSRF, encrypted-only queue, renewal, actual keeper publication
  and exact receipt confirmation. Only Privy ownership verification is mocked.
- `chat:test:ui`: two relocated browser origins with Orb API unavailable, new key
  recovery, encrypted names/history, same-block batch indexing, wallet-to-wallet
  message send/read, account switching, EN/PT/mobile and lazy Privy SDK loading.
- Four transactions with two controlled EOA wallets on **public** Monad testnet
  registered keys, published an archive and sent/read a DM. No customer messages
  or mainnet funds were used. Real Privy signing and another authorized origin
  still require human acceptance; fixture tests do not establish those results.

References: [Monad historical data](https://docs.monad.xyz/developer-essentials/historical-data),
[gas pricing](https://docs.monad.xyz/developer-essentials/gas-pricing),
[Privy app clients](https://docs.privy.io/basics/get-started/dashboard/app-clients),
[Privy global wallets](https://docs.privy.io/wallets/global-wallets/overview).

## What lands onchain, in plain words

Three different things get written to Monad testnet, and they are easy to confuse:

1. **Proofs, not messages.** Every epoch (a window of activity) the attestation
   keeper publishes a single Merkle root that commits to *what happened* - voice
   joins, mic flips, Arena wins, feed posts and chat messages. A root is a fingerprint: it proves a record existed and
   has not changed, and it costs one transaction per epoch no matter how many
   events are inside it. This runs **automatically**, for every community, as long
   as the operator runs the keeper. No community owner has to do anything.
2. **Ciphertext, when someone asks for a backup.** The portable archive stores
   your messages encrypted, publishes the encrypted blocks to replicas, and
   anchors their digests onchain so any reader can check the bytes came back
   unaltered. This is **not automatic**: it happens when a member opens the
   portable chat panel and backs up, or when the operator runs the archive index
   keeper for the communities it is configured to cover. The reason it is a
   decision and not a default is that every anchored byte costs gas from a wallet
   someone owns - silently spending an operator's wallet on every community's
   history would be a hidden fee, which this project refuses to have.
3. **Identity and ownership records.** Community registration, the Holder Pro
   licence and the passport proofs are per-community onchain records, written when
   the owner or member performs that action in the wallet.

So: **proofs are automatic and global; ciphertext backups are opt-in and paid for
by whoever runs the keeper; identity records are per action.** A community owner
who wants their community's chat to be recoverable without Orb has two honest
levers today: run the archive keeper themselves (section 4 of
[orb-self-hosting.md](orb-self-hosting.md), a folder and a gas-only wallet), or
ask their members to back up from the portable panel, which needs no server at
all. What Orb will not do is turn either on silently and bill someone for it.

The E2EE layer and the onchain layer are complementary, not contradictory: a
message is encrypted end-to-end before it is stored anywhere, and the *same*
ciphertext is what gets replicated and anchored. The chain never sees plaintext,
and a reader who recovers an archive decrypts it locally with their own key - the
proof onchain only says the ciphertext is the one the publisher committed to.
