diff --git a/docs/concepts/channel-access.md b/docs/concepts/channel-access.md deleted file mode 100644 index 62489e8..0000000 --- a/docs/concepts/channel-access.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -id: channel-access -title: Channel access -description: The five access models, and the membership contracts behind closed, gated and paid channels. ---- - -# Channel access - -Every channel has one of five access models, chosen at creation: - -
- -| | Open | Protected | Closed | Gated | Paid | -|---|---|---|---|---|---| -| Read access | Everyone | Password holders | Allowlisted addresses | Token / NFT holders | Active subscribers | -| Write access | Everyone | Password holders | Allowlisted addresses | Token / NFT holders | Active subscribers | -| Access enforced by | — (open by design) | Client-side encryption | On-chain gate contract | On-chain gate contract | On-chain gate contract | -| Content on the wire | Signed plaintext (Pombo format) | AES-256-GCM ciphertext | AES-256-GCM under a channel key | Same | Same | -| Cost to join | Free | Free | Free (owner pays gas to add you) | Holding the asset | The subscription price | - -
- -- **Open channels** are public rooms. Anyone can read and write. -- **Protected channels** look public to the network, but every message is encrypted client-side with AES-256-GCM using a key derived from the shared password (PBKDF2, 310,000 iterations). Without the password, the network carries only ciphertext. One caveat: each protected channel publishes a verification challenge that anyone can fetch and test guesses against offline — so the channel is exactly as secret as the password is strong. -- **Closed, Gated and Paid channels** enforce membership *on-chain*, through a small membership contract deployed per channel. This is the only family where access control is enforceable at the protocol level — the network itself refuses messages from authors the contract does not vouch for — and content is encrypted under a channel key that only members obtain. They differ only in the rule the contract enforces: an owner-managed allowlist, holding a token or NFT, or an active subscription. The rest of this page covers the three in detail. -- A channel can also be created **read-only** (announcement style): everyone can read, only the owner posts. - -:::note[Terminology] -Pombo's source code calls the pre-contract generation of closed channels *native channels*. Those still work, but can no longer be created: closed, gated and paid channels are all backed by a gate contract. -::: - -Who a channel belongs to, and what its owner can do about behaviour inside it, is the subject of [Ownership and moderation](ownership-and-moderation.md). - -## The membership contract - -Creating a closed, gated or paid channel deploys a **PomboGate** — a minimal membership contract, one [EIP-1167 clone](https://eips.ethereum.org/EIPS/eip-1167) per channel, from a shared factory. The clone costs a single transaction at creation and then answers two questions for the rest of the channel's life: - -- **`isValidSignature`** ([ERC-1271]) — *was this author ever a member?* This is what the Streamr Network checks on every message, which is why the clone, not you, is the channel's publisher on the wire. -- **`checkAccess`** — *does this address have access right now?* This drives encryption-key distribution and what the app shows you; it never invalidates messages already published. - -[ERC-1271]: https://eips.ethereum.org/EIPS/eip-1271 - -The consequence worth internalizing: **membership is sticky.** Being removed from an allowlist, letting a subscription lapse, or being banned cuts your *future* access; it does not retroactively unsign what you already wrote. Two things do reach backwards, and both are worth knowing before you write anything you'd hate to lose: an explicit owner `erase` (Closed channels only, where removing someone *is* the ban), and [selling a gate asset](#token-and-nft-gates) without having registered your membership on-chain. - -Because there is one grantee for every stream — the clone — adding or removing a member is a transaction on the gate, never a change to the channel's on-chain stream permissions. A Closed channel created with ten initial members costs one batched transaction, not ten. - -### Closed channels - -The owner (or a moderator) adds and removes Ethereum addresses. Members read and write for free; the owner pays the gas for each membership change. - -Closed channels are always **unlisted** — they never appear in Explore, and their name and description stay off-chain. This is the type to use for a private group. - -### Token and NFT gates - -The gate reads the member's balance at the moment access is checked: - -- **Token** — hold at least a minimum balance of a chosen ERC-20. The app offers POL, USDC and DATA as presets, or any contract address. -- **NFT** — hold at least one token of a chosen ERC-721. - -Nothing is paid to anyone and there is nothing to register: holding *is* the ticket, re-checked whenever it matters. Entering costs no transaction at all. Sell the asset and you lose access, exactly like someone who never held it. - -Selling has a second effect that is easy to miss. The same live check is what vouches for the messages you already published, so once your balance falls below the gate's, your past messages stop validating — and clients verify each message against the contract as they load it, so that history leaves the channel for every reader, not only for you. - -Registering your membership on-chain is the one-transaction opt-in that prevents this: it records you permanently, so the contract keeps vouching for you as an author whatever you later do with the asset. It does not extend your *access* — that ends when you sell either way. Only the standing of what you already wrote. - -### Paid channels - -The owner sets a price in an ERC-20 and a period in days. A subscriber pays, and the contract records an expiry timestamp. - -- **The payment goes to the channel owner.** The contract transfers the price straight to them; it takes no fee of its own, and Pombo is not a party to the transaction. -- **Renewing early loses nothing** — a renewal extends from the current expiry, not from the moment you pay. -- **There is no grace period.** Access ends at the recorded timestamp. The app warns you in the three days before, and shows an expired state with a renew action after. -- **Payment is first-person.** The contract deliberately refuses third-party payment: paying for someone else would link payer and beneficiary on-chain forever. -- For tokens that support [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) permits (such as USDC on Polygon), paying is a single transaction; otherwise it is the usual approve-then-pay pair. Prices set in POL are settled in wrapped POL, and the app wraps the shortfall for you. - -:::caution[A subscription buys the future, not the past] -New subscribers receive only the channel's **current** encryption key, so history published before they joined does not open for them — including files. This is deliberate: without it, one month's subscription would buy everything the channel ever said. See [Encryption](encryption.md#closed-gated-and-paid-channels). -::: - -## Honest limits - -- **Revocation is not instant.** Encryption keys rotate on a weekly cadence, so someone who sells the gate asset or lets a subscription lapse can still read new messages until the next rotation — and rotations happen only while the channel's admin is online. The contract cuts them off immediately for everything the *contract* controls; the key layer catches up afterwards. -- **The contracts have not been audited.** They are open source (see [pombo-contracts](https://github.com/Pombo-app/pombo-contracts)) and deliberately small, but no third party has reviewed them. -- **Paying is public.** Subscriptions, memberships and bans are contract state on a public blockchain: anyone can see which address pays for which channel, and when. -- **Authorship is not hidden.** The contract is the publisher on the wire, but your account's signature travels beside it in the clear, and it is what the network checks — so it cannot be encrypted, and anyone can recover who wrote what. Content stays private to members; participation does not. The reasoning is in the [privacy model](privacy-model.md#contract-backed-channels). diff --git a/docs/concepts/channel-anatomy.md b/docs/concepts/channel-anatomy.md deleted file mode 100644 index 48b8dae..0000000 --- a/docs/concepts/channel-anatomy.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -id: channel-anatomy -title: Anatomy of channels -description: The streams behind every channel and DM inbox — what each carries, what is stored, and where encryption applies. ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Anatomy of channels - -Every channel is a set of Streamr streams under the creator's address — three for open and protected channels, four when membership is contract-backed: - -| Stream | Stored? | Partitions | Purpose | -|---|---|---|---| -| `…-1` Message stream | Yes | 11 | Chat content (P0), edit/delete control messages (P1), file chunks (P2–P10) | -| `…-2` Ephemeral stream | No | 3 | Presence and typing (P0), live P2P media signals (P1) and data (P2) | -| `…-3` Admin stream | Yes | 3 | Moderation state (P0), channel image (P1), password challenge (P2) — writable only by the owner | -| `…-4` Keys stream | Yes | 1 | Distribution of the channel's encryption keys — closed, gated and paid channels only | - -The stream ID itself has the form `{ownerAddress}/{id}`, which is what makes ownership self-evident: the creator's address is part of the channel's name. What that ownership means in practice is the subject of [Channel access](channel-access.md) and [Ownership and moderation](ownership-and-moderation.md). - -The keys stream is stored on purpose: it is what makes joining a private channel asynchronous. A newcomer's key request waits there until some member comes online to answer it, instead of both having to be connected at the same moment. See [Encryption](encryption.md#closed-gated-and-paid-channels). - -The full picture per channel type — on-chain metadata, permissions, partitions, and where encryption applies (click to zoom): - - - - -![Open channel — multiple stream architecture: three streams with their on-chain metadata, permissions and partition layout](../assets/diagrams/open-channel.webp) - -*Content flows unencrypted; the four metadata variants cover visible/hidden and read-only combinations.* - - - - -![Protected channel — multiple stream architecture: identical stream layout, with every partition encrypted by a key derived from the shared password](../assets/diagrams/protected-channel.webp) - -*Same layout as an open channel, but every partition's content passes through AES-256-GCM with a PBKDF2-derived key (green path); the admin stream gains the password-challenge partition.* - - - - -Diagram coming soon — contract-backed channels add the keys stream to the layout above. Every stream's permission is held by one grantee, the channel's membership contract, and content is encrypted under a channel key distributed over `…-4`. See [The membership contract](channel-access.md#the-membership-contract). - - - - -## Direct messages: the mailbox model - -Each user has a personal **DM inbox** — a deterministic pair of streams derived from their address: a stored message stream (13 partitions: messages, sync, notifications, file chunks) and an ephemeral one for presence and typing. Its permissions are the inverse of a normal channel: **anyone can publish** into it, but **only the owner can subscribe** (read). The inbox metadata also carries the owner's encryption public key, which is what lets anyone seal a message to them. - -Sending a DM means encrypting a message for the recipient and dropping it into their inbox. Because the inbox is backed by a storage node, the recipient doesn't need to be online — they pull their inbox history when they next open the app. This is what makes Pombo's DMs asynchronous, like a phone's SMS inbox, rather than requiring both parties online. - -The inbox also doubles as your personal [cross-device sync](storage-and-sync.md#cross-device-sync) channel. - -![DM inbox — dual stream architecture: a stored inbox stream and an ephemeral stream, with ECDH-derived AES-256-GCM encryption on every partition](../assets/diagrams/dm-inbox.webp) - -*The inbox publishes the owner's encryption public key as on-chain metadata (green); every partition — messages, sync, notifications, file chunks — is sealed with a key derived via ECDH + HKDF before it touches the network.* diff --git a/docs/concepts/client.md b/docs/concepts/client.md deleted file mode 100644 index e871361..0000000 --- a/docs/concepts/client.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: client -title: Client & Protocol -description: The system in one narrative — a client on your device, Streamr for transport, storage nodes for history, Polygon for ownership. ---- - -# Client & Protocol - -Pombo has no message backend: no server receives, routes or authorizes your conversations. Everything you'd expect a backend to do is done either by the client on your device or by open infrastructure that anyone can run. The client sits on top of three layers: - -``` -┌─────────────────────────────────────────────┐ -│ Pombo client (browser PWA / Android app) │ -│ identity · encryption · UI │ -├─────────────────────────────────────────────┤ -│ Streamr Network → message transport │ -│ (P2P pub/sub) & delivery │ -├─────────────────────────────────────────────┤ -│ Storage nodes → message history │ -│ (Streamr + Cassandra) & offline delivery │ -├─────────────────────────────────────────────┤ -│ Polygon PoS → ownership & │ -│ (Streamr registries) permissions │ -└─────────────────────────────────────────────┘ -``` - -Follow one message through the system and the whole architecture appears. You type in a channel; the client encrypts the message if the channel calls for it, signs it, and publishes it to one of the channel's *streams* on the **Streamr Network** — a peer-to-peer pub/sub network, so it propagates directly between subscribers with no relay server in the middle. A **storage node** archiving that stream keeps a copy, which is how members who were offline read it later. And the reason the network accepted the message at all — who owns the channel, who may publish to it — is a record on **Polygon PoS**, public blockchain state rather than a row in someone's database. - -Each layer in one line: - -- **Transport — Streamr Network.** Messages are published to streams (topics) and propagate peer-to-peer between subscribers. -- **Ownership — Polygon PoS.** Streams are registered on-chain in the Streamr registry contracts. On-chain writes (creating a channel, granting membership) cost a small fee in POL; everything else is free. -- **Persistence — storage nodes.** Streamr nodes running the storage plugin retain stream history, so messages reach people who were offline. See [Storage & Sync](storage-and-sync.md). -- **Cryptography — your device.** Keys are generated and used locally. All encryption and signing happens client-side before anything is published. Two deliberate choices are worth knowing: the Streamr SDK's own encryption layer is **disabled** — all confidentiality is applied at the app layer, as [Encryption](encryption.md) describes — and channel messages are published under a per-channel throwaway key carrying a signed proof of the real account inside the payload (in contract-backed channels, the publisher is the membership contract instead; see [Privacy model](privacy-model.md)). - -Beyond these layers, the client talks to a small set of auxiliary services — public RPC endpoints, The Graph, the push relay ([Push notifications](notifications.md) explains that whole subsystem), the Explore curation manifest — none of which handle message content. The full list and what each one sees is in the [threat model](../security/threat-model.md). - -## The interface is replaceable - -Pombo (the app at app.pombo.cc) is *an* interface to this protocol, not *the* system. All of the state — identities, channels, permissions, history — lives on public networks. Anyone can build another client against the same streams, and if the Pombo interface vanished, the protocol and your data would remain. diff --git a/docs/concepts/encryption.md b/docs/concepts/encryption.md deleted file mode 100644 index 9d8ed95..0000000 --- a/docs/concepts/encryption.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -id: encryption -title: Encryption -description: How Pombo encrypts DMs (ECDH + sealed sender), protected channels, epoch-keyed private channels, and local state. ---- - -# Encryption - -All cryptography in Pombo runs **on your device**. The web app uses the browser's built-in WebCrypto for symmetric operations and audited Ethereum libraries for elliptic-curve operations; the Android app uses the platform's native crypto for symmetric operations and runs the same Ethereum library code for elliptic-curve operations, guaranteeing byte-for-byte compatibility between the two. Private keys never leave your device; the only key material ever published is your *public* key, attached to your DM inbox so others can encrypt to you. - -This page covers what encryption protects: the *content*. The separate question — who can see that it was **you** — is the subject of the [privacy model](privacy-model.md). - -## Direct messages: end-to-end, sealed sender - -DMs use the strongest protection in Pombo: - -1. **Key agreement — ECDH on secp256k1.** For each message, a *fresh ephemeral keypair* and the recipient's static public key derive a shared secret. The recipient's public key is published as metadata on their DM inbox, so no online handshake is needed. -2. **Key derivation — HKDF-SHA256** turns the shared secret into an encryption key. -3. **Encryption — AES-256-GCM** with a fresh random IV per message (authenticated encryption: tampering is detected). - -### Sealed sender - -On the wire, a DM doesn't reveal who sent it — not even to the network. Each message is sealed with a **fresh ephemeral keypair**: the network sees a throwaway address as the publisher, and the sender's real identity (a cryptographic proof of it) travels *inside* the encrypted envelope. Only the recipient, upon decrypting, learns who wrote to them. - -What this means concretely: a network observer watching your inbox can see *that* messages arrive (inboxes are public-write by design), but not *who* they're from or what they say. - -### Asynchronous by design - -Because encryption is pure key-derivation (no session handshake), the recipient can be offline for days: the sealed message waits in their stored inbox and decrypts whenever they return. Images and files sent in DMs are sealed the same way, with a fresh ephemeral key per transfer. - -### What that costs: no forward secrecy in DMs - -There is no ratchet, so message keys are not destroyed after use. What a stolen key opens follows the mailbox model, and it is not symmetric: - -- **Your key** opens the DMs you **received**, because those sit in your inbox. -- **The other person's key** opens the DMs you **sent**, because those sit in theirs. -- Your device also holds your own copy of what you sent, since you cannot read it back from the network. - -So a compromise reaches whatever is still retained, not only what follows it. Signal makes the opposite trade: its ratchet destroys each key immediately, and the price is that history cannot be recovered from the network at all. Pombo needs that recovery, because restoring a backup or opening a second device is exactly the act of deriving those keys again. - -Retention is the lever. A short one on your inbox shrinks what your key would expose; it does nothing for what you sent, which lives under someone else's retention. [The threat model](../security/threat-model.md#shrinking-what-a-seizure-would-find) covers how far that goes. - -Channels differ: their epoch keys do rotate, see [Rotation and history](#rotation-and-history) below. - -## Protected channels - -Protected-channel messages are encrypted with **AES-256-GCM** under a key derived from the shared password via **PBKDF2 (310,000 iterations, SHA-256)**. The network and storage nodes carry only ciphertext. Anyone who has the password can derive the key — the secrecy of the channel is exactly the secrecy of its password. - -## Closed, gated and paid channels - -These are encrypted with an **epoch key**: one AES-256-GCM key shared by the whole channel, versioned, and rotated over time. Every message carries the identifier of the key that sealed it, so a client picks the right one without trial decryption. The Streamr SDK's own group-key layer is not used. - -### How members get the key - -Getting the key is a small protocol of its own, running on the channel's keys stream: - -1. The channel admin **announces** each new epoch — its number, an identifier, and a hash of the key. The key itself is never announced. -2. A newcomer who passes the gate **requests** it, publishing a throwaway public key made fresh for that one request. -3. **Any member** already holding the key may answer, sealing it to that throwaway key (the same ECDH + HKDF + AES-256-GCM construction as a DM). -4. The newcomer accepts the key only if it hashes to what the admin announced. - -Three properties fall out of that design. Distribution does not depend on the admin — any member can answer, so a channel does not go dark when its owner sleeps. A malicious member cannot poison the key, only waste bandwidth, because the admin's hash is the anchor. And because the keys stream is *stored*, a request waits for an answer instead of requiring the two people to be online simultaneously. - -The one irreducible cost: between passing the gate and receiving the key there is a wait, during which the app shows the channel as waiting for keys. It is over as soon as any member's client sees the request. - -### Rotation and history - -Epoch keys rotate — on a weekly cadence in gated and paid channels, and whenever the owner removes someone. Rotation is what turns a contract-level revocation into an actual loss of access: a member who sells the gate asset or lets a subscription lapse keeps reading until the current epoch ends. - -How much history a new member receives depends on the channel: - -- **Closed, gated by token or NFT** — every retained epoch. Holding access *is* the condition, so there is nothing to withhold. -- **Paid** — the current epoch only. A subscription buys the future, never the channel's past. - -What the epoch key protects is the *content*. It does not conceal who wrote each message: authorship travels outside the encryption in these channels, necessarily — see [Privacy model](privacy-model.md#contract-backed-channels). - -## Open channels - -Open channels are **intentionally not encrypted** — they are public rooms, and their content is signed plaintext. What Pombo protects there is different: your network-level identity, via ephemeral publisher keys (see [Privacy model](privacy-model.md)). - -## What is protected locally - -Independently of transport encryption: - -- Your **private key** is stored encrypted at rest — on the web as a scrypt-encrypted keystore unlocked by your password; on Android in encrypted preferences under a device-bound key from the Android Keystore (no password — see [Account](../getting-started/identity.md)). -- The app's **state** (contacts, channel list, settings) is encrypted with AES-256-GCM and isolated per account. On the web the key derives from a deterministic wallet signature (PBKDF2, 310,000 iterations); on Android it is protected by the platform's native encrypted storage. -- In the web app, a few low-sensitivity items remain in plain browser storage (display name, ENS cache, which streams are registered for push). - -See [Account](../getting-started/identity.md). diff --git a/docs/concepts/notifications.md b/docs/concepts/notifications.md deleted file mode 100644 index 70ee11a..0000000 --- a/docs/concepts/notifications.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -id: notifications -title: Notifications -description: How Pombo delivers push notifications without learning who you are — wake signals, k-anonymity, a blind relay, and the per-platform quirks. ---- - -# Notifications - -Pombo can wake your device when a channel you follow or your DM inbox gets a message. Push is the hardest feature to do privately: the platforms (Google FCM, Apple APNs) sit in the delivery path by construction. Pombo's design goal is that **no party in the push path — relay, Google, or Apple — learns who is messaging whom, or what was said.** - -## How it works - -1. When someone messages a channel or inbox you follow, their client broadcasts a **wake signal** on a dedicated Streamr push stream — anyone can publish to it, but subscribing is restricted to the relay; it is not a publicly readable feed. The signal contains no message content — only a **1-byte tag** derived from the destination, plus a small proof-of-work to deter spam. It is also published under a throwaway key, so it doesn't reveal the sender either. -2. A **relay** (an open-source, community-runnable server) listens for wake signals, checks the proof-of-work, and fires a Web Push to *every* device registered under that tag. -3. Your device wakes and checks whether there's actually a message for you — the service worker queries the channel's storage node over HTTPS. Only then does a notification render; message content never travels through the push system. - -Push is opt-in per channel; your own DM inbox is registered automatically. Registrations refresh every 6 hours, and the proof-of-work is bound to a 10-second epoch so wake signals can't be replayed. The registration itself — the one message that carries your push endpoint — is **encrypted to the relay's public key** (published in the push stream's on-chain metadata and verified against the relay's known address before use), and is sent under a throwaway key: nothing on the stream links it to an account, and only the relay can read the endpoint inside. - -The full flow, with the k-anonymity collision made visible — Alice and Charlie share tag `0x75` for different channels, so both devices wake and each verifies locally; Bob's tag doesn't match, so his device never wakes (click to zoom): - -![Push notifications with k-anonymity: registration phase and wake-signal phase across sender, relay, FCM and recipients](../assets/diagrams/push-k-anonymity.webp) - -*Registrations and wake signals travel over the Streamr push stream; the relay maps tags to push subscriptions and fires Web Push; each woken device checks the storage node and only renders a notification if there really is a message for it.* - -The 1-byte tag is the key trick: with only 256 possible tags, many users share each one (**k-anonymity**). The relay knows "someone in bucket 173 has mail," not who. The cost is that your device occasionally wakes for someone else's message and silently goes back to sleep. - -## What the push path does and doesn't learn - -| Party | Learns | Does not learn | -|---|---|---| -| Relay | Tag bucket, aggregate frequency | Message content, sender, sender's IP, which user you are | -| Google / Apple | That your device runs Pombo | Content, sender, which channels/contacts you have | -| Network observer | Wake-signal timing (see caveat below) | Content, sender, recipient beyond the tag bucket | - -The honest residuals: platform push tokens inherently tell Google/Apple *that* you use Pombo. The relay stores your push token keyed to your tags, so it can link the notifications one device receives over time (though not who that device belongs to) — what it holds and for how long is stated in the [privacy policy](/legal/privacy-policy). The wake-verification step means the storage node sees which stream your device polls, and when. Registrations are encrypted to the relay's key, but wake signals travel unencrypted by design (they must stay cheap to send and carry nothing sensitive) — the push stream's subscribe permission is a network-level restriction, so a determined observer running a modified node could still watch wake-signal timing. The design assumes this: even a full view of the stream yields only tag buckets and timestamps. Small user bases also mean smaller anonymity sets. - -## Platform notes - -- **iOS**: notifications require the PWA to be **installed to the Home Screen** (iOS 16.4+). Safari-tab usage won't receive push. -- **Android app**: uses Firebase Cloud Messaging. Battery optimizations (Doze, Adaptive Battery) can delay delivery by minutes — exempting Pombo from battery optimization helps. -- **Relay availability**: today one community relay serves the network; if it's down, push pauses (messages are unaffected — they're waiting on the network). The relay is open source and anyone can run one — see [Run a push relay](../operators/run-a-relay.md). - -Even when everything is set up right, push is best-effort: delivery can lag from seconds to minutes depending on the platform. [Troubleshooting](../help/troubleshooting.md#im-not-receiving-push-notifications) has the checklist. - -## In-app notifications and invites - -Notifications *inside* Pombo (such as channel invites) don't use the push system at all — they're delivered through your DM inbox, end-to-end encrypted like any DM. diff --git a/docs/concepts/ownership-and-moderation.md b/docs/concepts/ownership-and-moderation.md deleted file mode 100644 index 6342bd8..0000000 --- a/docs/concepts/ownership-and-moderation.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -id: ownership-and-moderation -title: Ownership and moderation -description: On-chain ownership, what a ban is worth in each access model, and how discovery and curation work. ---- - -# Ownership and moderation - -## Ownership - -The channel's stream ID literally contains the creator's address: `{ownerAddress}/{id}`. Ownership and permissions are entries in the Streamr registry contracts on Polygon PoS — public blockchain state that no third party, including Pombo, can alter or confiscate. - -Two consequences worth understanding: - -- **Your channels survive Pombo.** Any client that speaks the protocol can read them. -- **Creating a channel is public.** The creator's address is permanently visible in the channel ID. If you don't want a channel linked to your main identity, create it from a separate account. - -## Moderation - -Owners moderate through the channel's admin stream, which only they can write to: **ban members, hide messages, pin messages**. Clients apply this state when rendering. - -How much a ban is worth depends entirely on the channel's [access model](channel-access.md): - -- In **open and protected channels**, moderation is *advisory*: compliant clients hide banned users' messages, but since accounts are free and instant, a banned user can return with a new address in one click — and the messages still exist on the network for non-compliant clients. In open channels the ban list itself is world-readable. -- In **contract-backed channels** (Closed, Gated, Paid), bans are the enforceable kind: the contract stops vouching for the address, and the network stops accepting its messages. This is the difference [that family](channel-access.md#the-membership-contract) exists for. - -Even an enforceable ban cuts *future* access without unsigning what the member already published. Removing their history is a separate, explicit owner action, available only in Closed channels. And their ability to *read* ends at the channel's next key rotation, not the instant of the ban — see the [honest limits](channel-access.md#honest-limits). - -In contract-backed channels the owner can also appoint **moderators**, who add, remove and ban members but cannot erase history, act on the owner or other moderators, or appoint anyone themselves. - -## Discovery and curation - -The **Explore** view lists channels that opted into discovery: listing is a choice at creation time, channels are **unlisted by default**, and closed channels are never listed. Gated and paid channels *can* be listed, as storefronts: the listing shows the channel's name, description, image and access marker — the condition or the price — so someone can see what they would be joining before acquiring anything, while the conversation itself stays encrypted; a shop window, not a preview. An unlisted channel publishes no name or description on-chain — only the owner address and creation time are visible. On top of that, the Pombo interface applies a curation manifest — a list of pinned channels and channels hidden *from discovery*. Hiding is interface-level only: a hidden channel still exists on the network and remains reachable by direct link. This reflects Pombo's design split: the *protocol* is neutral and uncensorable; the *interface* curates what it presents. diff --git a/docs/concepts/privacy-model.md b/docs/concepts/privacy-model.md deleted file mode 100644 index bc3a2c2..0000000 --- a/docs/concepts/privacy-model.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -id: privacy-model -title: Privacy model -sidebar_label: Privacy -description: Ephemeral publisher identities, sealed sender, and what an observer of the network can actually see. ---- - -# Privacy model - -Pombo's privacy design has one organizing idea: **your real account should not be visible on the wire unless the context deliberately makes it so.** [Encryption](encryption.md) protects *what you said*; this page is about who can see *that it was you*, and what an observer of the network actually learns. - -For the separate question of what data leaves your device and who receives it, see the [privacy policy](/legal/privacy-policy); for what an attacker can achieve, the [threat model](../security/threat-model.md). - -## Ephemeral publisher identities - -When you join a channel, Pombo generates **one throwaway keypair per channel** and uses it as your network-level publisher identity across all of that channel's streams. It is created on your first publish, never persisted, and discarded when you leave the channel or disconnect. The address the network sees is not your account. - -Your real identity travels as a **publisher proof** — a signature by your account over the ephemeral key — so that *other Pombo clients* can verify who you are and show your name. Where that proof travels is what varies by context: - -| Context | Publisher on the wire | Where the identity proof lives | Who learns your real account | -|---|---|---|---| -| Open channel | Ephemeral, rotates per join | Plaintext in the message | Anyone who parses the Pombo format | -| Protected channel | Ephemeral | Inside the AES envelope | Channel members only | -| Direct message | Ephemeral ([sealed sender](encryption.md#sealed-sender)) | Inside the ECDH envelope | The recipient only | -| Closed / gated / paid channel | The channel's membership contract | Your account's signature on the message itself | Anyone | -| Read-only channel (owner posts) | Your real account | — (not needed) | Anyone | - -So in a **protected channel**, an outside observer sees only ciphertext published by random throwaway addresses. In a **DM**, even your recipient's inbox reveals nothing about you to observers. In an **open channel**, your identity is readable — deliberately, because public rooms are public — but only at the application layer, not as raw wallet signatures on the transport. - -## Contract-backed channels - -Closed, gated and paid channels are the one place where the throwaway identity does not apply, and the reason is worth understanding rather than memorizing. - -Access there is enforced by the network itself: before accepting your message, nodes check its signature against the channel's membership contract. That check is what makes a ban stick. But it means the signature has to be **readable by parties that hold no key** — the network is not a member and never will be. So while the contract's address is what appears as the publisher, your account's signature travels beside it in the clear, and recovering your address from it is arithmetic anyone can do. - -The result is a clean trade rather than a leak. Content stays encrypted to members. Authorship is public: who wrote in the channel, and when. Enforceable access and publisher anonymity pull in opposite directions, and these channel types choose enforcement. - -:::note[Designed, not built] -A pseudonymous mode — proving membership once at the door, then publishing under a throwaway key with authorship moved inside the ciphertext — is designed and not implemented. It would trade some of the enforcement back for privacy, which is why it is a mode rather than a replacement. -::: - -The [threat model](../security/threat-model.md#visible-metadata) covers how far that authorship actually travels. - -## Anonymity = another account - -Pombo intentionally has **no per-channel "anonymous mode" toggle**. If you want to participate somewhere without linking it to your main identity, create another account — it's free, instant, and completely unlinked (each account's local data is isolated). This is a deliberate design decision: one strong, simple mechanism instead of a subtle toggle that's easy to get wrong. - -## Metadata protections that are on by default - -- **ENS lookups are decoyed** in the direction that runs constantly: resolving the name behind an address you see mixes the real lookup with decoy addresses, in shuffled order, so RPC operators can't tell which one you cared about. Resolving a name *you typed* — starting a DM, sending an invite — is not covered, and the provider sees exactly the name you asked for. -- **Push notifications carry no content** and use k-anonymity tags so the relay can't tell who a notification is really for — see [Push notifications](notifications.md). -- **Cross-device sync is sealed to yourself**: state snapshots published to your own inbox are encrypted so only your key can read them, and any payload not authored by your own wallet is rejected. -- **Network node IDs are not derived from your wallet.** - -## What this model does *not* hide - -Honest limits, in brief — the full list is in the [threat model](../security/threat-model.md): - -- Open channels are public: anyone can recover your account from the proof and correlate your activity across open channels. -- Contract-backed channels do not hide authorship — [see above](#contract-backed-channels). -- Read-only channels and moderation actions you perform as owner publish under your **real wallet**, as does your ownership of the channel itself. -- A channel creator's address is embedded in the channel ID forever. -- Membership of contract-backed channels is public blockchain state: who is allowlisted, who is banned, who paid for a subscription and until when. -- DM inboxes are enumerable, and their traffic pattern is public: given any address, anyone can find its inbox, and an unauthenticated request to the storage node returns its retained envelopes. When messages arrived and how many is readable by anyone. What that does *not* reveal is who they were from — every message carries a different throwaway publisher and sealed content ([threat model](../security/threat-model.md#visible-metadata)). -- Your IP address is visible to network peers, as in any P2P system. For now, use a VPN or Tor if IP privacy matters to you; a proxy-node layer built on Streamr Sponsorships is in development to address this at the protocol level. -- **ENS profile pictures are fetched from wherever their owner points them.** Seeing someone's avatar means your client requests an image from a server *they* chose, which learns your IP address — and someone can point their avatar at a server they run precisely to collect that. Pombo requires HTTPS and routes `ipfs://` avatars through a public gateway, neither of which hides who made the request. **Settings → Content → ENS Avatars** turns the fetch off entirely and falls back to the generated identicon; it is on by default, and off it holds everywhere an avatar is drawn, including the screens shown before you unlock and the pictures attached to notifications. diff --git a/docs/concepts/storage-and-sync.md b/docs/concepts/storage-and-sync.md deleted file mode 100644 index 500e4d9..0000000 --- a/docs/concepts/storage-and-sync.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: storage-and-sync -title: Storage & Sync -description: How history persists and devices converge — storage nodes, retention, local state, and self-encrypted sync. ---- - -# Storage & Sync - -The Streamr Network delivers messages live; it does not remember them. Persistence comes from **storage nodes** — Streamr nodes that archive stream history and serve it back on request. - -## Storage nodes - -When a channel (or DM inbox) is created, its stored streams are assigned to one or more storage nodes (changeable later in settings). When you open a channel, Pombo fetches recent history from the storage node, then follows the live stream. - -- **Default provider:** Pombo operates a storage cluster (two servers with a replicated Cassandra database behind a single node identity) that channels use out of the box. -- **Custom storage nodes:** channel owners can point a channel (or their own DM inbox) at any Streamr storage node instead. The node must be web-reachable: HTTPS with a real hostname, since browsers cannot fetch plain-HTTP or raw-IP endpoints. See [Run a storage node](../operators/run-a-storage-node.md). - -What storage nodes hold is exactly what the network carried: ciphertext for protected channels, contract-backed channels and DMs, signed plaintext for open channels. Storage operators are infrastructure, not custodians — they cannot read encrypted content. - -Reading is a direct HTTPS request from your device to the node, so whoever runs it sees which channels you open and when. Choosing a node is choosing who gets that. - -## Retention - -Channel owners choose how long storage nodes keep message history (1–365 days, default **180**; changeable later in channel settings). Older messages age out of storage; the live network never held them anyway. A DM inbox has the same setting, chosen by its owner — the [threat model](../security/threat-model.md#shrinking-what-a-seizure-would-find) covers retention as a privacy lever. - -## What lives on your device - -Everything personal starts local, and is encrypted at rest. With a DM inbox it also travels, sealed to your own key, so your other devices can read it — see [Cross-device sync](#cross-device-sync) below. - -- Your key and app state (contacts, channel list, settings, image ledger) are encrypted at rest and isolated per account — in the web app as a scrypt keystore plus AES-256-GCM-encrypted IndexedDB; on Android in the platform's encrypted storage under the Android Keystore. Detail in [Encryption](encryption.md#what-is-protected-locally). -- Channel messages are deliberately **not** cached locally — they are re-fetched from storage nodes each time you open a channel. -- Sent DMs are kept locally — a quirk of the mailbox model: your sent messages live in the *recipient's* inbox, so your own copy is your record of them. - -## Cross-device sync - -Your DM inbox doubles as a personal sync channel: the app periodically writes **self-encrypted snapshots** of your state to it. Log in on another device with the same account and it pulls those snapshots, merges them, and converges — contacts, channels, settings, sent messages (read state stays per-device). Only your key can decrypt them, and any payload not authored by your own wallet is rejected. - -## Files travel two ways - -Pombo moves files either through the storage node (chunked, archived, downloadable for the retention period) or over a live P2P mesh between online peers (nothing archived, someone must seed). You choose per file when sending — [File sharing](../guides/file-sharing.md) has the comparison and limits. diff --git a/docs/getting-started/first-steps.md b/docs/getting-started/first-steps.md deleted file mode 100644 index b57819d..0000000 --- a/docs/getting-started/first-steps.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -id: first-steps -title: First steps -description: Join a channel, send your first message, and DM someone. ---- - -# First steps - -## Join a channel - -Channels are Pombo's group spaces. You can reach them three ways: - -- **Explore**, the in-app discovery view, which lists the channels their owners chose to list. -- **A direct link**: any channel can be shared as a URL. Opening it takes you straight there. Try the official Pombo channel: [app.pombo.cc/#/channel/…](https://app.pombo.cc/#/channel/0xae340e799e8151f6a4999d245e466197aa217667/9862eb7bd898f338-1) -- **The channel ID**: paste it into **Join Channel**. Every channel has one, in the form `0x…/name`, and it works whether or not the channel is listed. - -Channels are [unlisted by default](../concepts/ownership-and-moderation.md#discovery-and-curation), and an unlisted channel never appears in Explore. For those, a link or the channel ID is the only way in. - -There are five kinds of channels; the app shows which is which: - -| Type | Who can read | Who can write | -|---|---|---| -| **Open** | Everyone | Everyone | -| **Protected** | Anyone with the shared password | Anyone with the password | -| **Closed** | Addresses the owner allowlists | The same | -| **Gated** | Anyone holding the required token or NFT | The same | -| **Paid** | Anyone with an active subscription | The same | - -More detail in [Channel access](../concepts/channel-access.md). - -## Send a message - -Type and send. Messages propagate peer-to-peer over the Streamr Network and are persisted by storage nodes so people who join later (or come back online) can read the history. Reactions, edits, deletes, and replies work as you'd expect. - -## Direct messages - -Start by creating your **DM inbox**. It is what lets you receive messages and replies, and what keeps your devices in sync. Setting it up registers streams on-chain, so it costs a small POL fee: once, and never again. - -With that done, you can write to anyone who has an inbox of their own. Enter their address or ENS name and send. They don't need to be online, and there is no friend request to accept first. - -DMs are end-to-end encrypted with sealed sender: only the recipient can read the message, and only the recipient learns who sent it. The full picture — the inbox, blocking, where messages live — is in [Direct messages](../guides/direct-messages.md). - -## Create your own channel - -Creating a channel registers streams on Polygon PoS, which costs a small network fee in POL, one of the handful of on-chain actions that do (the [FAQ](../help/faq.md#is-pombo-free) has the full list). See [Managing channels](../guides/managing-channels.md). - -## Before you go further - -:::tip -Two minutes now saves your account later: [export a backup](../guides/backup-and-recovery.md). There is no account recovery without it. -::: diff --git a/docs/getting-started/identity.md b/docs/getting-started/identity.md deleted file mode 100644 index ac8e10f..0000000 --- a/docs/getting-started/identity.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -id: identity -title: Account -description: How Pombo accounts work — locally generated keypairs, no sign-up, no recovery service. ---- - -# Account - -*Your identity and keys* - -A Pombo account is an **Ethereum keypair generated locally on your device**. Creating one costs nothing, requires no email or phone number, and never touches a server. - -Your address (`0x…`) is your identity on the network: it's how people DM you, how channel permissions are granted to you, and how your channels are owned by you. - -## How your key is protected - -- On the **web**, your private key is stored encrypted, and unlocked with the password you choose. -- On **Android** there is no password: the key is protected by the device itself, through its hardware keystore. The honest trade-off is that anything running *as the Pombo app* on an unlocked device can use the key (this is what lets push notifications decrypt in the background); sensitive screens are gated by your device lock. - -Everything else the app stores — contacts, channels, settings — is also encrypted, separately for each account. The mechanisms are in [What is protected locally](../concepts/encryption.md#what-is-protected-locally). - -## There is no "Forgot password" - -:::danger[Back up your account] -Nobody holds your key. If you lose your device and have no backup, the account — and everything encrypted with it — is gone permanently. - -Export a backup file as soon as you create your account: see [Backup and recovery](../guides/backup-and-recovery.md). -::: - -## Multiple accounts and guest mode - -- You can create and switch between **multiple named accounts** on the same device, each with its own isolated data. -- **Guest mode** gives you a throwaway account with no persistence — useful for having a look around without creating anything. - -Because accounts are free and instant, switching accounts is also Pombo's answer to pseudonymity: rather than a per-channel "anonymous mode", you simply use a different account for contexts you want to keep separate. See the [privacy model](../concepts/privacy-model.md#anonymity--another-account) for why this matters. - -## Using an existing wallet - -You can import an existing Ethereum private key into Pombo. Connecting an external wallet (MetaMask, hardware wallets) without importing the key — via a signed delegation — is on the roadmap but **not yet available**. - -## ENS names - -If your address has an ENS name, Pombo resolves and displays it. Lookups go to public Ethereum RPC endpoints; Pombo adds decoy queries so the RPC operator can't tell which address you were actually interested in. diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md deleted file mode 100644 index 00548f1..0000000 --- a/docs/getting-started/install.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -id: install -title: Install Pombo -description: How to install Pombo on the web, Android and iOS. ---- - -# Install Pombo - -## Web (any platform) - -Pombo runs entirely in your browser at **[app.pombo.cc](https://app.pombo.cc)** — nothing to download. - -To install it as an app (recommended, and required for push notifications on iOS): - -1. Open [app.pombo.cc](https://app.pombo.cc#install). -2. Use your browser's **Install app** / **Add to Home Screen** option. - - **Chrome / Edge (desktop and Android):** look for the install icon in the address bar, or *Menu → Install app*. - - **Safari (iOS 16.4+):** *Share → Add to Home Screen*. Push notifications on iOS only work for installed PWAs. - -Installing gives you an app icon, a full-screen UI, and — on iOS — the ability to receive push notifications. - -## Android app - -An Android app — a native Kotlin/Jetpack Compose UI running the same protocol stack as the web app — is available from the [GitHub repository](https://github.com/Pombo-app/Pombo-Android). It is wire-compatible with the web app — same account format, same channels, same encryption — and uses Firebase Cloud Messaging for push notifications. - -Requirements: Android 8.0 (API 26) or newer. - -## After installing - -Your next step is creating an identity — see [Account](identity.md). There is no sign-up: the app generates a keypair locally the first time you use it. diff --git a/docs/guides/backup-and-recovery.md b/docs/guides/backup-and-recovery.md deleted file mode 100644 index fe6bcd7..0000000 --- a/docs/guides/backup-and-recovery.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -id: backup-and-recovery -title: Backup and recovery -description: Export a portable, encrypted backup of your account — the only recovery mechanism that exists. ---- - -# Backup and recovery - -:::danger[The one rule] -Nobody holds your key and there is no custodian, so there is **no "forgot password", no account recovery, no support ticket** that can bring an account back. Your backup file *is* the recovery mechanism. Make one when you create your account, and keep it somewhere safe. -::: - -## Exporting a backup - -In settings, export your account backup. You get a single portable file containing: - -- your **encrypted key**, -- your **encrypted app state** (contacts, channels, settings, your sent messages), and -- the **encryption keys of the private channels you belong to**. - -The file is encrypted under your **account password** (verified before export) — without it, the file is useless to whoever finds it. Treat it with the same care as the key itself. - -The backup deliberately carries only what cannot be recovered from elsewhere: anything the network still holds is left out and re-downloaded on restore. Media you sent in DMs is the one judgement call — it lives only in the recipient's inbox, so it is included by default, behind a toggle you can turn off to keep the file small. - -:::info[Why channel keys are in there] -Paid channels never re-distribute past keys, and the announcements that anchor older ones expire with the channel's retention. For history in those channels, your backup is the only recovery path — see [Encryption](../concepts/encryption.md#rotation-and-history). -::: - -## Restoring - -On any device — new phone, fresh browser, after a reinstall — import the backup file and enter your account password. Your identity and data are restored exactly. - -## What backup does *not* cover - -- **Received message history** isn't in the backup — it lives on the network and re-downloads from storage nodes (within each channel's retention window). The same goes for images you received. -- A backup is a snapshot: state changes after export aren't in it. For continuously-synced state across devices, just log in on both — see below. - -## Multi-device sync - -You don't need to shuttle backup files between devices you use actively. Import your account once on each device; from then on, Pombo syncs state automatically through your own DM inbox — including your private channels' encryption keys, so a second device doesn't have to re-request them — self-encrypted so only your key can read it (see [Cross-device sync](../concepts/storage-and-sync.md#cross-device-sync)). - -## Practical advice - -- Export a backup **immediately after creating your account**, before it has anything to lose. -- Re-export occasionally, or after big changes (new channels you own, important contacts). -- The backup opens with your **account password** — losing that is the same as losing the backup. Store both well. diff --git a/docs/guides/direct-messages.md b/docs/guides/direct-messages.md deleted file mode 100644 index 7540ebc..0000000 --- a/docs/guides/direct-messages.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -id: direct-messages -title: Direct messages -description: Set up your DM inbox, message anyone, and know what a public mailbox does and doesn't mean. ---- - -# Direct messages - -DMs in Pombo travel live, peer-to-peer, when you're both online — and behind every conversation sits a mailbox: each message also lands in the recipient's inbox, where it waits if they're away. Neither of you ever needs to be online at the same time. - -## Set up your inbox - -To *receive* DMs you need a **DM inbox** of your own. Creating it registers streams on-chain, which costs a small POL fee — once, and never again. The inbox is more than a mailbox: it is also where channel invites arrive, and it is what [keeps your devices in sync](../concepts/storage-and-sync.md#cross-device-sync). - -Two things to know before creating it: - -- **Sync starts with it.** Once the inbox exists, Pombo automatically publishes self-encrypted snapshots of your state to it — contacts, channels, settings — so a second device can converge. Only your key can read them, and you can set sync to manual in Settings. -- **Guest mode has no inbox**, so guests can't receive DMs or sync devices. - -## Send a message - -Enter the recipient's address or ENS name and write. They don't need to be online, and there is no friend request to accept first — anyone with an inbox can be written to. Images and files ride along, sealed the same way as the text. - -What protects a DM: end-to-end encryption with **sealed sender** — only the recipient can read the message, and only the recipient learns who sent it; the network sees ciphertext from a throwaway address. The mechanics, and the one trade-off worth knowing (there is no forward secrecy in DMs), are in [Encryption](../concepts/encryption.md#direct-messages-end-to-end-sealed-sender). - -## Living with a public mailbox - -Your inbox is **public-write by design** — that is what lets a stranger answer your channel post or accept your invite without a handshake. The honest consequence is that anyone can write to you, including people you'd rather not hear from, and there is no server-side filter to change that. - -What you *can* do is **block a peer**: your clients stop showing you anything from them, and the block list syncs across your devices. Blocking is client-side — it hides, it cannot stop a deposit from arriving at a public mailbox. The wider spam question is an [open problem](../security/threat-model.md#known-open-problems) the threat model states plainly. - -## Where your messages live - -The mailbox model has one asymmetry worth understanding: - -- **What you receive** sits in *your* inbox, under the storage node and [retention period](../concepts/storage-and-sync.md#retention) you set. -- **What you send** sits in the *recipient's* inbox, under their node and their retention — which is why your own device keeps a local copy of your sent messages: it is your only record of them. - -History you receive re-downloads from storage on any device; if privacy matters more than convenience, a short retention on your inbox shrinks what it holds — the [threat model](../security/threat-model.md#shrinking-what-a-seizure-would-find) covers that trade. diff --git a/docs/guides/file-sharing.md b/docs/guides/file-sharing.md deleted file mode 100644 index 96a5419..0000000 --- a/docs/guides/file-sharing.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -id: file-sharing -title: File sharing -description: Pombo's two file transports — live P2P mesh and persistent storage-based sharing. ---- - -# File sharing - -Pombo can move files two ways, and **you choose which** when sending. Knowing the difference helps you pick the right one. - -## Persistent sharing (via storage nodes) - -For files that should outlive the moment: the sender's client splits the file into small chunks, publishes them to the channel's stored stream across several partitions, verifies they were archived, and then posts the file announcement into the chat. - -- **Receivers can download any time** within the channel's retention period — the sender can go offline immediately after upload. -- Chunks are small (~240 KB) because that's the practical per-message limit for browser-to-network transport; large files simply mean many chunks. -- In **DMs**, file content is sealed end-to-end like everything else: a fresh ephemeral key per transfer, readable only by the recipient. - -## Mesh sharing (live P2P) - -For live transfer between online peers: a pull-based swarm over the channel's ephemeral (unstored) stream, similar in spirit to BitTorrent — receivers request pieces, and anyone who has pieces can serve them. - -- **Requires at least one online seeder.** If everyone with the file goes offline, the transfer can't complete. -- Individual files are capped at 500 MB; seeded files are kept for re-serving for up to 7 days, in a local seed cache capped at 700 MB. -- Nothing is written to storage nodes — the transfer leaves no archived trace. - -## Files in private channels - -In closed, gated and paid channels, both transports are sealed with the channel's encryption key — the same one that protects messages — so storage nodes and the network carry ciphertext either way. - -One consequence to expect: because those keys rotate, **a member who joins after a rotation cannot open files shared before it**, exactly as they cannot read the messages from that period. In a paid channel that is the rule rather than an edge case — a subscription buys the future. See [Encryption](../concepts/encryption.md#rotation-and-history). - -## Managing transfers - -Downloads in progress live in the notification bell. Completed ones save automatically. While a transfer runs you can **pause and resume** it — both transports — and storage downloads can also be **cancelled**, which discards what was fetched (a pause keeps it). When you finish seeding a file, the bytes stay as an inactive record you can re-seed later; deleting them is a separate, explicit action. - -## Which is which? - -| You want to… | Transport | -|---|---| -| Post a file to a channel for people to grab later | Persistent | -| Send an image in a DM | Persistent (sealed) | -| Push something big to people who are online right now | Mesh | diff --git a/docs/guides/managing-channels.md b/docs/guides/managing-channels.md deleted file mode 100644 index 89f4373..0000000 --- a/docs/guides/managing-channels.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -id: managing-channels -title: Managing channels -description: Create a channel, choose its type and retention, manage members and moderate. ---- - -# Managing channels - -## Creating a channel - -Creating a channel registers its streams on Polygon PoS, which costs a network fee (a few cents' worth of POL — the app estimates the cost and checks your balance before starting). You'll choose: - -- **Type**: open, protected, closed, gated or paid. Pick based on who should get in; the trade-offs are explained in [Channel access](../concepts/channel-access.md). Any type can also be made **read-only** (only you post). The last three deploy a membership contract as part of creation, which adds one transaction to the cost — see [The membership contract](../concepts/channel-access.md#the-membership-contract). -- **Visibility** — whether the channel is listed in Explore. Channels are unlisted by default; listed channels also carry a description, language and category. -- **Retention** — how long storage keeps history (1–365 days, default 180). -- **Storage node** — the default Pombo cluster, or a custom storage node identified by its Ethereum address (the app verifies on-chain that the node publishes an HTTPS endpoint browsers can reach). - -Retention and storage nodes can be changed later in channel settings (each change is an on-chain transaction). - -The channel is owned by the account that creates it. Ownership is on-chain and cannot be transferred away from you by anyone. - -## Managing members (closed channels) - -In a closed channel, you control access by **adding and removing Ethereum addresses** in the channel's membership contract. Each change is a Polygon transaction paid by you (the owner); adding several people at once is a single transaction. Members join and chat for free. - -This is the channel type to use when a ban has to actually stick: once you remove someone, the network itself refuses their messages. Two things to know before relying on it — what they already published stays published unless you also erase it, and their ability to *read* ends at the channel's next key rotation, not the instant you remove them. Both are explained in [Channel access](../concepts/channel-access.md#the-membership-contract). - -In gated and paid channels there is no member list to manage: the contract's rule — holding the asset, or an active subscription — decides. What you manage there is bans and, if you want the help, moderators. - -## Moderation tools - -As owner, you can: - -- **Ban members** — their messages stop being displayed by clients, and in contract-backed channels they lose access outright. -- **Hide individual messages.** -- **Pin messages.** -- **Appoint moderators** (contract-backed channels) — they manage membership and bans, but cannot erase history, act on you or other moderators, or appoint anyone themselves. - -Moderation state is published on the channel's admin stream, which only you can write to. - -:::caution -In open and protected channels, remember that bans are advisory: a determined user can rejoin with a fresh account, and the underlying messages remain on the network. For enforceable access control, use a contract-backed channel. See [Moderation](../concepts/ownership-and-moderation.md#moderation). -::: - -## Inviting people - -- **Share the channel link** — anyone opening it lands in the channel (for protected channels, they'll also need the password; share it through a secure path such as a Pombo DM). -- **In-app invites** — sent through the recipient's DM inbox, end-to-end encrypted like any DM. diff --git a/docs/help/faq.md b/docs/help/faq.md deleted file mode 100644 index cd63639..0000000 --- a/docs/help/faq.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -id: faq -title: FAQ -description: Frequently asked questions about Pombo. ---- - -# FAQ - -Privacy questions — can Pombo read my messages, is it anonymous, who sees my IP — have their own page: [Privacy at a glance](../security/privacy-at-a-glance.md). - -### Is Pombo free? - -Pombo itself has no ads, no premium tier and no cut of anything. Chatting, DMs and file sharing are free; the costs are small Polygon network fees (paid in POL, typically cents) for **on-chain actions**: creating a channel, the one-time creation of your personal DM inbox (needed to *receive* DMs and to sync devices), managing members in a closed channel, and changing an existing channel's retention or storage nodes. - -Two things a channel's *creator* can charge for, if they choose: a **gated** channel asks you to hold a token or NFT, and a **paid** channel asks for a subscription. That money goes to the channel owner — the contract takes no fee and Pombo is not a party to it. Everything else stays free, and if you only join open channels and chat, you pay nothing. - -### What happens if pombo.cc disappears? - -Your channels, messages, and identity live on the Streamr Network and Polygon — not on Pombo's website. The app is open source; any client implementing the protocol can access the same data. The interface is replaceable; your data outlives it. - -### Do I need a crypto wallet or tokens to use Pombo? - -No. Pombo generates its own keypair for you — no MetaMask, no seed phrase ceremony, no purchase. You only need a few cents of POL for on-chain actions such as creating a channel or the one-time setup of your DM inbox (and, if you want into a gated or paid channel, whatever asset that channel's owner asks for). You can also import an existing Ethereum private key if you want your established identity. - -### Are there group chats? - -Group conversation in Pombo is a **channel**. There is no separate "group DM" primitive — a private group is a protected or closed channel. - -### Someone I banned came back with a new account. Why? - -Because accounts are free and unlinked, a ban in an open or protected channel can't stop a determined user — this is a structural property of permissionless systems, and Pombo is honest about it rather than pretending otherwise. If you need enforceable access control, use a **closed, gated or paid channel**, where membership is contract state you control and the network refuses messages from anyone the contract won't vouch for. - -### How is this different from Signal? From Matrix? From Farcaster? - -Briefly: **Signal** is the gold standard for content encryption, but it requires a phone number — a real-world identity anchor — and runs on central servers you have to trust to stay up and neutral; Pombo asks for no identifier at all and has no backend to trust. **Matrix** federates servers; Pombo has no backend to federate — transport is P2P and state is on-chain. **Farcaster**-style social protocols are public-first; Pombo is messaging-first with E2EE DMs and encrypted channels. Pombo's particular corner is: no sign-up, no servers, creator-owned channels, sealed-sender DMs. diff --git a/docs/help/glossary.md b/docs/help/glossary.md deleted file mode 100644 index 446d402..0000000 --- a/docs/help/glossary.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: glossary -title: Glossary -description: Pombo and Streamr terminology, defined. ---- - -# Glossary - -**Account** — an Ethereum keypair generated locally by Pombo. Your address is your identity; no registration exists anywhere. - -**Admin stream** — the third stream of every channel, writable only by the owner, carrying moderation state (bans, hidden messages, pins). - -**Channel** — Pombo's group space: a set of Streamr streams owned by its creator (messages, ephemeral, admin — plus a keys stream when membership is contract-backed). - -**Closed channel** — a channel whose members are an allowlist in its own on-chain membership contract, managed by the owner. Called a *native channel* in Pombo's source code, which is the earlier generation of the same idea. - -**DM inbox** — your personal mailbox stream, derived from your address and created on-chain (a one-time small fee). Anyone can deposit (publish); only you can read (subscribe). It publishes your encryption public key so others can seal messages to you, and carries your encrypted cross-device sync data. - -**Ephemeral publisher key** — the throwaway keypair Pombo uses as your network-level identity in a channel session, so your real account never appears on the wire. Discarded when you leave. - -**Ephemeral stream** — a channel's unstored stream: presence, typing indicators and live media transfer. Nothing published here is archived. - -**Epoch key** — the encryption key shared by the members of a closed, gated or paid channel. Versioned and rotated over time; a member holds every epoch they were given, and messages name the one that sealed them. - -**Gate (PomboGate)** — the membership contract deployed with each closed, gated or paid channel. It answers two questions: was this author ever a member (which is what the network checks on every message), and does this address have access right now (which decides key distribution and what the app shows). - -**Gated channel** — a channel whose gate admits anyone holding a chosen token balance or NFT. No payment is involved: holding is the ticket. - -**Guest mode** — a throwaway account with no persistence, for looking around. Guests have no DM inbox, so no DMs and no sync. - -**k-anonymity tag** — the 1-byte destination hint in push wake signals. With only 256 buckets, many users share each tag, so the relay can't tell who a notification is for. - -**Keys stream** — the fourth stream of a contract-backed channel, where epoch keys are announced, requested and handed out. Stored, so a join doesn't require another member to be online at that moment. - -**Mesh sharing** — live P2P file transfer between online peers over the ephemeral stream. Needs an online seeder. - -**Moderator** — an address the owner of a contract-backed channel appoints to manage membership and bans. Cannot erase history, act on the owner or other moderators, or appoint further moderators. - -**Paid channel** — a channel whose gate admits anyone with an active subscription: a price in a chosen token, per period, paid directly to the channel owner. New subscribers receive only the current epoch key, so the channel's past does not open for them. - -**Persistent sharing** — storage-node-backed file transfer: files are chunked into the channel's stored stream, downloadable for the retention period with the sender offline. - -**POL** — Polygon PoS's native currency, used for the small network fees on on-chain actions (creating channels, your DM inbox, managing members, changing retention or storage nodes). - -**Protected channel** — a channel encrypted client-side with a key derived from a shared password (PBKDF2 → AES-256-GCM). The network sees only ciphertext. - -**Publisher proof** — a signature by your real account over your ephemeral publisher key, letting other Pombo clients verify who you are. Public in open channels; sealed inside the encryption envelope in protected channels and DMs. Contract-backed channels don't need it: there, your account signs the message itself. - -**Relay (push relay)** — a community-runnable server that converts Streamr wake signals into Web Push notifications, blind to content and identities. - -**Retention** — how long a storage node keeps a stream's history; chosen by the channel owner, 180 days by default. - -**Sealed sender** — Pombo's DM envelope: the sender's identity travels inside the ciphertext, so even the recipient's inbox doesn't reveal who wrote until the recipient decrypts. - -**Storage node** — a Streamr node (typically backed by Cassandra) that archives stream history and serves it back, enabling offline delivery and history. - -**Stream** — Streamr's pub/sub primitive: a named, on-chain-registered topic that peers publish to and subscribe from. Everything in Pombo is built from streams. - -**Streamr Network** — the decentralized P2P pub/sub network Pombo uses as its message transport. - -**Wake signal** — a contentless push trigger broadcast when someone messages you: just a k-anonymity tag plus proof-of-work. Your device wakes and fetches the real message from the network. - diff --git a/docs/help/troubleshooting.md b/docs/help/troubleshooting.md deleted file mode 100644 index 9c2ae32..0000000 --- a/docs/help/troubleshooting.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: troubleshooting -title: Troubleshooting -description: Common issues and how to resolve them. ---- - -# Troubleshooting - -## I'm not receiving push notifications - -Work through these in order: - -1. **iOS:** push only works when Pombo is **installed to the Home Screen** (iOS 16.4+), not in a Safari tab. Reinstall via *Share → Add to Home Screen* and re-enable notifications. -2. **Android:** battery optimization (Doze, Adaptive Battery) delays or drops background pushes. Exempt Pombo from battery optimization in system settings. -3. **Notifications toggled on?** Check both the in-app setting and the OS-level permission for the app/site. -4. **Relay availability:** push depends on a community relay being online. If everything on your side looks right, the relay may be down — messages still arrive when you open the app; only the wake-up is affected. - -Note that even when everything works, push is best-effort: delivery can lag by seconds to minutes depending on the platform's mood. - -## Channel history is missing or incomplete - -- History older than the channel's **retention period** (default 180 days) has expired from storage — that's by design. -- If *recent* messages appear and disappear between reloads, the storage node is having replica-consistency issues. There's nothing to fix client-side; it usually resolves when the operator repairs the node. Messages you watched arrive live are unaffected on your device. -- Channels on **custom storage nodes** depend on that node being up and correctly configured (HTTPS, valid certificate). If the owner's chosen node is unreachable, history won't load until it returns. - -## A transaction failed when creating a channel or managing members - -On-chain actions need a little POL for gas on Polygon PoS: - -- Check your balance — a few cents' worth is enough. Note the app estimates the cost up front and **blocks creation below the estimate**, so "nothing happens" usually means insufficient balance, not a failure. -- Make sure you're not rejecting the transaction prompt. -- Public RPC endpoints occasionally rate-limit; retrying after a moment typically succeeds (the app rotates between several endpoints). - -## I forgot my password / lost my device - -If you have a **backup file**, import it and enter your account password — everything comes back. If you don't, the account cannot be recovered by anyone, including us; that's the flip side of no one else holding your keys. Create a new account and [back it up this time](../guides/backup-and-recovery.md). - -## The app seems out of sync between my devices - -Sync propagates through your DM inbox and applies on app open — give both devices a moment online. If a device has been offline longer than your inbox's retention, it may miss intermediate snapshots; re-importing a fresh backup file resyncs it fully. diff --git a/docs/operators/run-a-relay.md b/docs/operators/run-a-relay.md index fc1cf8d..be506d8 100644 --- a/docs/operators/run-a-relay.md +++ b/docs/operators/run-a-relay.md @@ -1,20 +1,20 @@ --- id: run-a-relay title: Run a push relay -description: Run a Pombo push relay — the easiest way to contribute infrastructure to the network. +description: Run a Pombo push relay, the easiest way to contribute infrastructure to the network. --- # Run a push relay -The push relay bridges Streamr wake signals to Web Push (FCM/APNs). It's deliberately **blind** — it never sees message content or user identities (see [Push notifications](../concepts/notifications.md)) — and deliberately **cheap to run**. This is the easiest piece of Pombo infrastructure to operate. +The push relay bridges Streamr wake signals to Web Push and FCM. It is deliberately blind, it never sees message content or user identities (see [Notifications](../protocol/notifications.md)), and deliberately cheap to run. This is the easiest piece of Pombo infrastructure to operate. ## Requirements -- Node.js 18+ -- ~50–100 MB RAM, ~100 MB disk (SQLite is embedded — no database server) +- Node.js 18 or newer +- About 50 to 100 MB of RAM and 100 MB of disk. SQLite is embedded; there is no database server. - A stable internet connection -That's Raspberry Pi territory. A home PC, a free-tier cloud VM, or the cheapest VPS you can find all work. +That is Raspberry Pi territory. A home PC, a free-tier cloud VM, or the cheapest VPS you can find all work. ## Setup @@ -23,22 +23,22 @@ git clone https://github.com/Pombo-app/Pombo-push cd Pombo-push npm install cp .env.example .env -npx web-push generate-vapid-keys # → VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY +npx web-push generate-vapid-keys npm start ``` -Fill in `.env` before starting: paste the generated VAPID keys, and set `RELAY_PRIVATE_KEY` to a fresh Ethereum private key (any wallet tool can generate one — use a dedicated key, not a personal wallet). Configuration reference: +Fill in `.env` before starting: paste the generated VAPID keys, and set `RELAY_PRIVATE_KEY` to a fresh Ethereum private key. Use a dedicated key, not a personal wallet. | Variable | Purpose | |---|---| | `RELAY_PRIVATE_KEY` | The relay's Streamr identity | -| `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` | Web Push (VAPID) keypair | +| `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY` | Web Push (VAPID) keypair | | `VAPID_EMAIL` | Contact email included in push requests | -| `POW_DIFFICULTY` | Proof-of-work requirement for wake signals (default 4) | -| `PUSH_STREAM_ID` | The Streamr stream to listen on — **must be** `0xae340e799e8151f6a4999d245e466197aa217667/push` for the live Pombo network. The code's fallback default is a placeholder: with it, the relay starts cleanly, listens to an empty stream, and silently never delivers anything. | -| `ADMIN_PORT` | Admin API port (default 8000; set `0` to disable the admin API entirely) | +| `POW_DIFFICULTY` | Proof-of-work requirement for wake signals, default 4 | +| `PUSH_STREAM_ID` | The Streamr stream to listen on. For the live Pombo network it must be `0xae340e799e8151f6a4999d245e466197aa217667/push`. The code's fallback default is a placeholder: with it the relay starts cleanly, listens to an empty stream, and never delivers anything | +| `ADMIN_PORT` | Admin API port, default 8000. Set `0` to disable the admin API entirely | -One consequence of sealed registrations: clients encrypt the registration to the relay key **published in the push stream's on-chain metadata**, and verify it against the relay address configured in the app. A relay with a different key never receives readable registrations on the live stream — so today, running your own relay means running it against your own push stream (and client configuration), not piggybacking on the live one. Contributing a relay to the main network is a coordination step with the project rather than a config value. +One consequence of sealed registrations: clients encrypt the registration to the relay key published in the push stream's on-chain metadata, and verify it against the relay address configured in the app. A relay with a different key never receives readable registrations on the live stream. Today, running your own relay means running it against your own push stream and client configuration, not piggybacking on the live one. Contributing a relay to the main network is a coordination step with the project rather than a config value. ## Keeping it running @@ -49,14 +49,14 @@ pm2 start index.js --name pombo-relay pm2 save && pm2 startup ``` -The admin API (`GET /admin/stats`, `GET /admin/registrations`, `GET /admin/events`) shows registration counts and delivery stats. To confirm the relay actually works end-to-end: enable notifications on a device in Pombo, then check `curl localhost:8000/admin/stats` shows the registration, and send yourself a message from another account. +The admin API (`GET /admin/stats`, `GET /admin/registrations`, `GET /admin/events`) shows registration counts and delivery stats. To confirm the relay works end to end: enable notifications on a device in Pombo, check that `curl localhost:8000/admin/stats` shows the registration, then send yourself a message from another account. :::caution -The admin API is **unauthenticated**, binds to all interfaces, allows any origin, and includes a destructive `DELETE` endpoint. Firewall the port to localhost, or set `ADMIN_PORT=0` and use the offline `admin-cli.js` tool instead. +The admin API is unauthenticated, binds to all interfaces, allows any origin, and includes a destructive `DELETE` endpoint. Firewall the port to localhost, or set `ADMIN_PORT=0` and use the offline `admin-cli.js` tool instead. ::: ## Operational notes - **VAPID keys are the relay's identity to browsers.** If you rotate them or users switch relays, their devices must re-register for push. Back up your `.env` and `data/tokens.db`. -- The relay stores only push subscriptions (endpoint + tag), never message data. +- The relay stores only push subscriptions (endpoint and tag), never message data. - Resource usage grows very slowly with users; a single small instance comfortably serves thousands of registrations. diff --git a/docs/operators/run-a-storage-node.md b/docs/operators/run-a-storage-node.md index a2b126e..1a9fc56 100644 --- a/docs/operators/run-a-storage-node.md +++ b/docs/operators/run-a-storage-node.md @@ -1,62 +1,59 @@ --- id: run-a-storage-node title: Run a storage node -description: Operate a Streamr storage node that Pombo channels can use for message persistence. +description: Operate a Pombo storage node that channel owners can point their channels at. --- # Run a storage node -Storage nodes archive stream history so channels and DM inboxes work asynchronously. Pombo channel owners can point their channels at **any Streamr storage node** — including yours. This is a heavier commitment than a [push relay](run-a-relay.md): it's a database-backed, always-on service. +Storage nodes archive stream history so channels and DM inboxes work when people are offline. A channel owner can point a channel at any provider, including yours. This is a heavier commitment than a [push relay](run-a-relay.md): a database-backed, always-on service whose uptime the channels assigned to it depend on. -:::info -A storage node is standard Streamr infrastructure, not Pombo-specific software. The authoritative reference is the [Streamr documentation](https://docs.streamr.network); this page covers the setup profile that makes a node usable from Pombo. -::: +The node software is [pombo-storage-node](https://github.com/Pombo-app/pombo-storage-node), a build of the Streamr storage node that validates writes at ingest, serves private history only to signed requests, stamps each message with a receipt time, deletes on request and enforces retention on its own. It stays a drop-in Streamr node otherwise. What each of those does for a channel is in [Storage and sync](../protocol/storage-and-sync.md). -## Components +## What you need -A production storage node stack: +- A machine with Docker, 4 GB of RAM and a few tens of GB of disk, growing with the channels you host. +- A public IP with ports 80 and 443 (HTTPS endpoint) and 32200 (Streamr overlay) reachable from the internet. +- A hostname you control, with a DNS A record pointing at the machine. The web app runs in a browser and reads only from an `https://` endpoint with a valid certificate on a real hostname. An IP address or plain HTTP will not work. +- A little POL on the node's address, for the one transaction that registers it. -- **Streamr node** with the storage plugin (Node.js), exposing its HTTP API locally (default port 8002) -- **Apache Cassandra 4.1.x** (on Java 11) as the message store -- **A reverse proxy (nginx or Caddy) terminating TLS** in front of the node's HTTP API +## Install -## The web-safe profile (required for Pombo) +One command on a bare Debian, Ubuntu or RHEL-family machine: -These requirements are set by the strictest Pombo client — the web app, which runs in a browser and is bound by browser rules about what it may fetch. A node that meets them works for every client, the Android app included. Your node's public endpoint **must** be: - -- **HTTPS** with a valid certificate, -- on a **real hostname** — not an IP literal, not `localhost`, -- publicly reachable, -- answering with **CORS headers**: `Access-Control-Allow-Origin: https://app.pombo.cc` (plus `Vary: Origin`), configured on the reverse proxy. +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/Pombo-app/pombo-storage-node/pombo/103.3.1/bootstrap.sh) +``` -An `http://` endpoint or a bare IP will simply never be contacted by the web client, and without the CORS header the browser blocks every request even when everything else is right — the most common "it should work but doesn't" case (the Android app is not subject to CORS, so a node failing only that check still appears to work from Android — easy to miss when testing). Register the HTTPS URL as the node's metadata so clients can discover it on-chain. +The installer installs Docker, fetches the node, and asks for what only you can provide: whether to generate a key or use yours, and your hostname. It pulls the prebuilt image, pauses for you to fund the node's address with POL and open the firewall ports, registers the node on-chain, and brings up the node, its Cassandra database and a Caddy proxy that obtains the certificate. -## Setup outline +The same procedure by hand, for troubleshooting or a custom setup, is in the repository's [installation guide](https://github.com/Pombo-app/pombo-storage-node/blob/pombo/103.3.1/HOW_TO_INSTALL.md). -1. **Cassandra first.** Install Cassandra 4.1.x and create the keyspace and tables the storage plugin expects — the plugin does **not** auto-create its schema. The schema (keyspace plus `bucket` and `stream_data` tables) is documented in the Streamr storage plugin docs. -2. **Streamr node.** Install and configure the node with the storage plugin enabled, pointed at your Cassandra, with an Ethereum identity (private key) for the node. -3. **TLS proxy.** Put nginx/Caddy with a certificate in front of port 8002. Never expose 8002 itself publicly. -4. **Firewall.** Open only: SSH, 80/443 (proxy), and the Streamr node's P2P WebSocket port (32200 by default — check your node config). Cassandra's ports (7000, 9042, 7199) must never be publicly reachable. -5. **Register on-chain** so clients can discover your endpoint, and verify: +When it is up, from another machine: ```bash -npx -p @streamr/cli-tools streamr storage-node register "https://your-node.example" \ - --private-key --env polygon - -npx -p @streamr/cli-tools streamr storage-node show \ - --private-key --env polygon +curl https://node.example.org/capabilities ``` -A quick self-check from the outside: the certificate is valid, and `curl -I -H "Origin: https://app.pombo.cc" https://your-node.example/...` returns the `Access-Control-Allow-Origin` header. +answers with the node's name and feature list. A channel owner who enters your node's address under **Storage Provider → Custom** when creating a channel, or later in the channel's settings, gets its history stored with you. + +## Operating it + +- **Retention is automatic.** The node prunes data past each stream's retention on its own timer. There is no cron to set up. +- **Back up the node's private key.** Its address is how streams are assigned to it; losing the key orphans every channel pointed at your node. +- **Snapshot Cassandra before risky changes** with `nodetool snapshot`. +- **Upgrades** are a `git pull` and a compose restart. Schema changes ship as files; the node refuses to start when a column it needs is missing and names the file to apply. +- **Uptime matters more than specs.** A modest VPS with reliable disk beats a big machine that reboots weekly. + +## Running a cluster + +Several nodes can share one replicated Cassandra database under one provider identity, splitting the write load while any node serves any read. The installer has a cluster branch that asks the cluster size, whether this machine is the seed, and the IPs. The manual procedure, and what to check when the Cassandra ring will not form across machines, is in the repository's [cluster guide](https://github.com/Pombo-app/pombo-storage-node/blob/pombo/103.3.1/deploy/CLUSTER.md). -Channel owners can then select your node's address when creating a channel in Pombo. +Two things learned running Pombo's own cluster: -## Operational realities +- Ports 7000 and 9042 must be reachable between the nodes and from nowhere else. Cassandra is unauthenticated in this setup. +- Schedule **full** repairs, `nodetool repair -full -pr`, daily and staggered across nodes. Incremental repair, the default, leaves tombstones unable to meet their data, so disk is never reclaimed, and unrepaired replicas silently diverge. The symptom is history that appears and disappears between reloads, because alternate reads hit alternate replicas. -Lessons from running Pombo's own cluster: +## Registering more than one URL -- **Retention is not enforced automatically.** Streams declare a retention period, but expired data must be actively purged — and the bundled `delete-expired-data` job in `@streamr/node` 103.3.1 is broken, so run your own purge job and monitor disk usage. -- **If you run replicated Cassandra, schedule repairs — and use full repairs.** Run `nodetool repair -full -pr ` regularly (daily, staggered across nodes). Incremental repair (the default without `-full`) marks SSTables as repaired into a separate compaction pool, so tombstones never meet their data and disk space is never reclaimed. Unrepaired replicas also silently diverge — the symptom is *intermittently* missing history, because alternate reads hit alternate replicas. This is the most confusing failure mode you will meet. -- **Pombo clients probe an optional `format=metadata` query** that vanilla nodes answer with HTTP 400 — that's fine, the client falls back automatically. Nodes patched to support it serve file-transfer verification dramatically faster, but no action is required. -- **Back up the node's private key.** The node's identity is how streams are assigned to it; losing it orphans every stream pointed at your node. -- Uptime matters more than specs: channels assigned to your node depend on it for history. A modest VPS with reliable disk beats a big machine that reboots weekly. +A provider can register several HTTPS URLs at once, comma-separated at registration time, if it serves the same database from more than one hostname. Clients rotate across them and fail over when one stops answering. diff --git a/docs/protocol/access.md b/docs/protocol/access.md new file mode 100644 index 0000000..a33dc11 --- /dev/null +++ b/docs/protocol/access.md @@ -0,0 +1,50 @@ +--- +id: access +title: How access works +description: Which layer enforces reading, writing and history in a contract-backed channel, and why the identity mode is a one-time choice. +--- + +# How access works + +Open and protected channels enforce access by client convention and a shared password. Closed, gated and paid channels, the contract-backed family, enforce it with contract state the owner controls. The rules are in [Channel types](../reference/channel-types.md) and [The gate contract](contract.md). This page is about where each rule is enforced, and where it is not. + +## Three cuts, three layers + +Losing access does not cut everything at once. Each layer holds its own line. + +| | Enforced by | Visible mode | Sealed mode | +|---|---|---|---| +| **Writing** | The network and the storage node, at ingest | The gate validates every signature. Selling the token, an expired subscription, a revocation or a ban all stop writing within the gate cache window | The network sees only the shared publish key and never consults the gate. An ex-member who kept that key can keep writing until the owner resets it | +| **Reading** | Key distribution | No channel key, no plaintext. The gate is consulted on every key answer | Same | +| **History** | The storage node, at ingest, and the client, on read | A Pombo storage node validates signature and publish permission before storing, and refuses the history of the channel to anyone the gate does not accept now | Same, except that authorship is invisible to the node by design | + +Streamr's subscribe permission is not enforced on the live read path. What keeps a non-member from reading is the key, and what keeps them from fetching stored ciphertext is the signed read a Pombo storage node requires. + +## What you wrote stays written + +A message is validated when it is published and never re-checked when it is read. Losing access cuts your future and leaves the thread intact: members keep reading what a departed member wrote, and the departed member keeps whatever their client already holds. + +## Identity on the wire + +Contract-backed channels are the one place where Pombo's throwaway publisher identity does not apply. Anything the network checks has to be readable by non-members, so per-message enforcement and publisher anonymity cannot both hold. The creator picks which one gives, once, with the **Identity on the wire** setting: + +- **Sealed**, the default, drops the per-message check. Everyone publishes under one shared key. Authorship travels inside the encryption, as a session pseudonym signed per message and bound to your account by a proof only members can read. +- **Visible** keeps the check and pays in privacy. Your account signs each message, the network validates it against the contract, and anyone can recover your address from that signature. + +The contract holds the choice for the life of the channel, and Explore shows it on the card, so someone about to pay sees the trade before entering. What each mode reveals is in [Publisher identity](identity.md). + +## Read-only channels + +Read-only is fixed at creation too. Each mode enforces it with the property that already defines it: + +- **Sealed**: the shared publish key goes to the owner and moderators only. The content key and the interactions key go to every member. +- **Visible**: the gate accepts every member's signature by design, because a contract cannot tell streams apart, so the cut is made by the Pombo storage node, which refuses to store a message on the conversation stream from anyone but the owner and moderators. +- **Open and protected**: there is no moderator role, so the owner posts alone. + +Members still react and show as online in every type, because [reactions have a stream of their own](streams.md). + +## Protected channels + +A protected channel looks public to the network and carries only ciphertext, under a key derived from the shared password. Verification is local: the channel publishes a challenge, and a candidate password is tested against it on your device. The password never touches the network. + +The challenge is public, so a protected channel is exactly as secret as its password is strong. The [threat model](../security/threat-model.md) puts a number on that. diff --git a/docs/protocol/contract.md b/docs/protocol/contract.md new file mode 100644 index 0000000..32bf44f --- /dev/null +++ b/docs/protocol/contract.md @@ -0,0 +1,67 @@ +--- +id: contract +title: The gate contract +description: "PomboGate, one clone per contract-backed channel: what it answers, who may call what, and what is immutable." +--- + +# The gate contract + +Creating a closed, gated or paid channel deploys a **PomboGate**: one [EIP-1167 clone](https://eips.ethereum.org/EIPS/eip-1167) per channel, from a shared factory, for a single transaction. Source: [pombo-contracts](https://github.com/Pombo-app/pombo-contracts). + +It answers one question for the rest of the channel's life: **does this address have access right now?** + +Two callers ask it, and they get the same answer: + +| Caller | Function | Used for | +|---|---|---| +| Pombo clients | `checkAccess` | Handing out encryption keys, and what the interface shows you | +| The Streamr Network | `isValidSignature` ([ERC-1271]) | Accepting or refusing a message at ingest | + +[ERC-1271]: https://eips.ethereum.org/EIPS/eip-1271 + +Because the contract is the grantee on every stream, adding or removing a member is a transaction on the gate, never a change to stream permissions. A closed channel created with ten members costs one batched transaction, not ten. + +## The rule it enforces + +| Mode | Access means | +|---|---| +| **Closed** | On the owner's allowlist | +| **Gated, token** | Holding at least a minimum balance of an ERC-20 | +| **Gated, NFT** | Holding at least one token of an ERC-721 | +| **Paid** | A subscription that has not expired | + +The owner always has access; a banned address never does; moderators have access by role. + +## Who may call what + +| Action | Owner | Moderator | +|---|---|---| +| Add or remove allowlisted addresses | Yes | Yes | +| Ban and unban | Yes | No | +| Appoint moderators | Yes | No | +| Set price and duration | Yes | No | +| Publish in a read-only channel | Yes | Yes | + +Moderators cannot act on the owner or on each other. + +## Immutable after creation + +`wireIdentity` (sealed or visible) and `readOnly` are set at creation and can never change. The contract is the authority on both; the copy in the stream metadata is a cache. + +Price and duration are the owner's to change; the mode, the token and the minimum balance are not. + +## Paid channels + +- The payment is a **single transfer to the owner**. The contract takes no fee and never custodies the money. +- **Renewing early extends from the current expiry**, not from the moment you pay. +- **No grace period.** Access ends at the recorded timestamp; the app warns for three days before and shows a renew action after. +- **First-person only.** Paying for someone else is refused, because it would link payer and beneficiary on-chain forever. +- Tokens supporting [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) permits, such as USDC on Polygon, pay in one transaction; others use approve-then-pay. Prices set in POL settle in wrapped POL, and the app wraps the shortfall for you. + +## One deliberate asymmetry + +`checkAccess` is strict: a gate token that reverts makes it revert, and clients treat that as fail-closed, because a broken token answering "yes" would hand out encryption keys. + +`isValidSignature` fails **open** on the same fault: a broken token accepts the write. A frozen channel is unrecoverable, while the worst case here is unreadable ciphertext in storage. + +The contracts are open source, small, not upgradeable once deployed, and **not audited**. diff --git a/docs/protocol/encryption.md b/docs/protocol/encryption.md new file mode 100644 index 0000000..5bf7d97 --- /dev/null +++ b/docs/protocol/encryption.md @@ -0,0 +1,85 @@ +--- +id: encryption +title: Encryption +description: DMs with sealed sender, protected channels, channel keys and their distribution, and what is protected locally. +--- + +# Encryption + +All cryptography runs on your device: WebCrypto and ethers.js on the web, and the same protocol code in a headless WebView on Android, where signatures are made natively so the private key never enters the WebView. Private keys never leave the device. The only key material ever published is your *public* encryption key, attached to your DM inbox so others can encrypt to you. + +This page is about protecting content. Who can see that it was you is [Publisher identity](identity.md). The parameters are in [Limits and defaults](../reference/limits.md). + +## Direct messages + +Three steps per message: + +1. **Key agreement.** A fresh ephemeral keypair and the recipient's static public key derive a shared secret over ECDH on secp256k1. The recipient's key is published as inbox metadata, so no online handshake is needed. +2. **Key derivation.** HKDF-SHA256 turns that secret into an AES key. +3. **Encryption.** AES-256-GCM with a fresh IV, so tampering is detected. + +There is no session handshake, so the recipient can be offline for days: the sealed message waits in their inbox and opens when they return. + +### Sealed sender + +A DM does not reveal its author, not even to the network. Each message is published under a fresh throwaway address, and the sender's identity travels inside the encrypted envelope. Only the recipient, on decrypting, learns who wrote. An observer watching an inbox sees that messages arrive, since inboxes are public-write by design, but not from whom. + +### No forward secrecy + +There is no ratchet, so message keys are not destroyed after use. A stolen key opens whatever is still retained, and the mailbox model makes that asymmetric: + +- **Your key** opens the DMs you received, which sit in your inbox. +- **The other person's key** opens the DMs you sent, which sit in theirs. + +Signal makes the opposite trade: its ratchet destroys each key immediately, and history cannot be recovered from the network. Pombo needs that recovery, because restoring a backup or opening a second device is the act of deriving those keys again. Retention is the lever: a short retention on your inbox shrinks what your key would expose, and does nothing for what you sent. + +## Protected channels + +Content is AES-256-GCM under a key derived from the shared password with PBKDF2-SHA256. The network and storage nodes carry only ciphertext, and anyone with the password derives the key. + +## Contract-backed channels + +Content is encrypted with an **epoch key**: one AES-256-GCM key shared by the whole channel, versioned, and rotated over time. Every message names the key that sealed it, so a client picks the right one without trial decryption. The Streamr SDK's own group-key layer is not used. + +### How members get the key + +A small protocol on the [keys stream](streams.md): + +1. An admin **announces** each new epoch: its number, an identifier, and a hash of the key. The key itself is never announced. +2. A newcomer who passes the gate **requests** it. The request carries a throwaway public key made for that request and the account's own static public key. +3. **Any member** holding the key may answer, sealing it to the requester with the same construction as a DM. Answerers rank themselves by a hash so they do not all reply at once. +4. The newcomer adopts the key only if it hashes to what the admin announced. + +Distribution does not depend on the admin, so a channel does not go dark when its owner is away. A malicious answer wastes bandwidth and nothing more, because the announced hash is the anchor. The keys stream is stored, so a request waits for an answer instead of requiring two people online at once, and because the request carries the account's static key, an answer written days later still opens after the requesting device restarted. + +An owner or moderator can mark a device as the channel's **key responder**, which keeps answering pending requests while the app is open. + +**A new member receives every epoch the channel still retains.** Holding access now is the whole condition. Keys never go back to someone the gate no longer admits, and a superseded key does not open messages written after it. + +### Rotation + +| Trigger | When | +|---|---| +| A member is removed or banned | At once | +| Access lost another way: sale, expiry, revoke | Next time an admin opens the channel | +| Nothing in particular | Weekly | +| The owner presses **Rotate Now** | At once | + +Every path needs an admin present, so a channel nobody administers does not rotate. Rotation is what turns a contract-level revocation into an actual loss of reading; how long that lag can be is in the [threat model](../security/threat-model.md). + +### Two more keys, in sealed channels + +Both ride the same stream, announced and wrapped exactly like an epoch key, and checked against the announced hash: + +- The **shared publish key**, which everything on the conversation stream is written under. It does not rotate on a schedule; replacing it is the owner's **Reset Publish Key**, two transactions. In a read-only channel only the owner and moderators receive it. +- The **interactions key**, which covers the ephemeral and interactions streams and goes to every member with access, including those who cannot post. That is what lets members of a read-only channel react and show as present. + +## Open channels + +Open channels are deliberately not encrypted: they are public rooms, and their content is signed plaintext. What Pombo protects there is your network-level identity, through the channel pseudonym. + +## What is protected locally + +- Your **private key**: a scrypt-encrypted keystore unlocked by your password on the web; on Android, encrypted preferences under a hardware-backed key, with no password. See [Account](../use/account.md). +- The app's **state**, contacts and channels and settings: AES-256-GCM, isolated per account, under a key derived from a deterministic wallet signature on the web, and the platform's encrypted storage on Android. +- On the web, a few low-sensitivity items stay in plain browser storage: display name, ENS cache, and which streams are registered for push. diff --git a/docs/protocol/identity.md b/docs/protocol/identity.md new file mode 100644 index 0000000..e1efa1e --- /dev/null +++ b/docs/protocol/identity.md @@ -0,0 +1,41 @@ +--- +id: identity +title: Publisher identity +description: Who appears as the publisher on the wire in each context, where your account proof travels, and the metadata protections that are on by default. +--- + +# Publisher identity + +[Encryption](encryption.md) protects what you said. This page is about who can see that it was you. The rule: **your account is not visible on the wire unless the context deliberately makes it so.** + +## The channel pseudonym + +When you join a channel, Pombo generates **one throwaway keypair per channel** and publishes under it across that channel's streams. It is created on your first publish, never persisted, and discarded when you leave. The address the network sees is not your account. + +Your account travels as a **publisher proof**: a signature by your account over the throwaway key, so other Pombo clients can attribute the message and show your name. Where that proof sits depends on the context: + +| Context | Publisher on the wire | Proof lives | Who learns your account | +|---|---|---|---| +| Open channel | Channel pseudonym | Plaintext in the message | Anyone parsing Pombo's format | +| Protected channel | Channel pseudonym | Inside the AES envelope | Members only | +| Direct message | Fresh throwaway, sealed sender | Inside the ECDH envelope | The recipient only | +| Contract-backed, sealed | The shared publish key | Inside the epoch envelope | Members only | +| Contract-backed, visible | The gate contract | Your signature on the message | Anyone | +| Read-only open or protected, the owner's posts | The owner's account | Not needed | Anyone | + +Three things always travel under your real account: **creating a channel**, whose creator address is part of the channel ID; **the owner's moderation**, published on the admin stream only the owner may write; and **entering or staying in a contract-backed channel**, whose key requests and roster entries are account-signed so that members can check the gate before handing over a key. A moderator's actions travel as signed deltas on the message stream, under the channel's identity on the wire like any other message. Whether that identity is sealed or visible in a contract-backed channel is the creator's one-time choice, explained in [How access works](access.md). + +## Anonymity is another account + +Pombo has no per-channel anonymous mode. To take part somewhere without linking it to your main identity, use a different account. Accounts are free, instant, and each one's local data is isolated. + +## On by default + +- **ENS reverse lookups are decoyed.** Resolving the name behind an address you see mixes the real lookup with decoys, in shuffled order. Resolving a name *you typed*, to start a DM or send an invite, is not covered, and the provider sees exactly what you asked for. +- **Push notifications carry no content** and use k-anonymity tags, so the relay cannot tell who a notification is for. See [Notifications](notifications.md). +- **Cross-device sync is sealed to yourself**, and any payload not authored by your own wallet is rejected. +- **History reads are signed** for contract-backed channels and for your DM inbox, so a Pombo storage node serves that history only to the people it is for. The provider you read from gets a signed record of which private channels you open. See [Storage and sync](storage-and-sync.md). +- **Network node IDs are not derived from your wallet.** +- **ENS avatars can be turned off.** Showing someone's avatar fetches an image from a server *they* chose, which learns your IP. **Settings → Content → ENS Avatars** turns the fetch off and shows the generated identicon instead, everywhere an avatar is drawn. + +What none of this hides is listed once, in the [threat model](../security/threat-model.md). diff --git a/docs/protocol/moderation.md b/docs/protocol/moderation.md new file mode 100644 index 0000000..04b0033 --- /dev/null +++ b/docs/protocol/moderation.md @@ -0,0 +1,69 @@ +--- +id: moderation +title: Ownership and moderation +description: What ownership means on-chain, what each part of a ban does, who can erase what from storage, and how moderators work without the owner's key. +--- + +# Ownership and moderation + +## Ownership + +A channel's stream ID contains its creator's address, and its permissions are entries in the Streamr registry contracts on Polygon PoS: public state that no third party, Pombo included, can alter or confiscate. + +Two consequences. Your channels survive Pombo, because any client speaking the protocol reads them. And creating a channel is public: the creator's address is in the ID forever, so a channel you do not want linked to your main identity has to be created from a separate account. + +## What a ban does + +A ban is three independent effects, offered as three checkboxes: + +| | Where it lives | Cost | Effect | +|---|---|---|---| +| **Hide their messages** | The owner's snapshot on the admin stream, or a moderator's delta on the message stream | Free, reversible | Every Pombo client stops rendering what they write from the ban onward. The bytes stay on the network | +| **Cut their access** | The gate contract | One transaction, contract-backed only | No further keys, and the channel key rotates at once | +| **Erase their messages from storage** | A signed request to each of the channel's storage providers | Free, irreversible | Their messages are deleted from storage, and hidden on the way | + +Cutting access alone expels and leaves the history readable to members: clients validate a message when it arrives and never again. Hiding alone silences: the person goes on publishing and nobody sees it. Neither is a shadow ban, because the list is published on the channel's own streams and anyone in the channel can read it. + +A hide carries the **key generation** it was applied in, not a timestamp: messages travel with the identifier of the key that sealed them and that cannot be forged, while a timestamp is the publisher's to choose. So hiding someone covers what they write from that point on, and what they wrote before stays as it was. Open and protected channels have no key generations, so a ban there hides everything. + +Whether cutting access also stops the person from *writing* depends on the channel's identity on the wire; see [How access works](access.md). + +## Erasing from storage + +Hiding is a promise every Pombo client keeps. Erasing is a deletion the storage provider performs. + +| Who | Which message | Action | Reversible | +|---|---|---|---| +| The author | Their own | **Delete** | No | +| Owner or moderator | Any | **Hide**, then **Unhide** | Yes | +| Owner or moderator | Any | **Erase from storage** | No | +| Owner | Everything by a banned member | The third ban checkbox | No | + +**Per provider, reported as such.** A channel can hold several providers, so the request goes to each and the result is "erased on two of three". A provider without the capability keeps its copy, and the option is hidden unless at least one provider has it. + +**Hide first, erase second.** A client already holding the message shows it until a hide arrives, so erasing always hides on the way. + +**The author's Delete erases only while the proof holds.** The provider needs proof of authorship, which is the key that signed the message: + +| Channel | The author's Delete reaches storage | +|---|---| +| Visible | Always | +| Open, protected | Only during the session that published it, because the signing key is a throwaway | +| DM | Only during the session that sent it, for the same reason | +| Sealed | Never: the shared key proves nothing about who wrote | + +Delete still removes the message from every Pombo client in all four cases. In a sealed channel the owner or a moderator can erase what an author cannot. + +## Moderators + +The owner of a contract-backed channel can appoint moderators. They manage the allowlist, hide messages, erase them from storage and ban at the client level. They cannot ban on the contract, act on the owner or on each other, appoint anyone, or rotate keys. Open and protected channels have no contract, so no moderator role: their owner moderates alone. + +Only the owner can write to the admin stream, so a moderator's actions travel as **signed deltas** on the message stream, and clients render the owner's snapshot with the unabsorbed deltas applied on top. Moderation keeps working while the owner is away, and the owner has the last word on return: **Confirm Moderator Actions** absorbs the deltas into the owner's snapshot, and dismissing a moderator dissolves whatever they left pending. What was already absorbed stays. + +## Discovery and curation + +Explore lists the channels that opted in. **Listed** is a choice at creation, channels are unlisted by default, and closed channels are never listed. + +Gated and paid channels can be listed as storefronts: the card shows name, description, image, the access condition or price, and whether authors are visible on the wire, so someone sees what they would be joining before acquiring anything. The conversation itself stays encrypted. An unlisted channel publishes no name or description on-chain; only the owner address and creation time are visible. + +On top of that, the Pombo interface applies a curation manifest: a list of pinned channels and channels hidden from discovery. Hiding is interface-level only, and a hidden channel remains reachable by direct link. The protocol is neutral; the interface curates what it presents. diff --git a/docs/protocol/notifications.md b/docs/protocol/notifications.md new file mode 100644 index 0000000..bc5fb0b --- /dev/null +++ b/docs/protocol/notifications.md @@ -0,0 +1,45 @@ +--- +id: notifications +title: Notifications +description: "Waking a device without learning who it belongs to: wake signals, k-anonymity, a blind relay." +--- + +# Notifications + +Push is the hardest feature to do privately, because the platforms, Google FCM and Apple APNs, sit in the delivery path by construction. Pombo's goal is that **no party in that path learns who is messaging whom, or what was said.** + +## How it works + +1. When someone messages a channel or inbox you follow, their client broadcasts a **wake signal** on a dedicated Streamr push stream. It carries no content: a 1-byte tag derived from the destination, plus a small proof-of-work against spam, published under a throwaway key so it does not reveal the sender either. Anyone may publish to that stream; only the relay may subscribe. +2. The **relay**, an open-source server anyone can run, checks the proof-of-work and fires a Web Push to *every* device registered under that tag. +3. Your device wakes and checks whether there is really a message for it, by querying the channel's storage node over HTTPS. Only then does a notification render. + +Push is opt-in per channel; your own DM inbox is registered automatically. The registration itself, the one message carrying your push endpoint, is **encrypted to the relay's public key**, published in the push stream's on-chain metadata and verified against the relay's known address before use, and it is sent under a throwaway key. Nothing on the stream links it to an account, and only the relay can read the endpoint inside. + +![Push notifications with k-anonymity: registration and wake-signal phases across sender, relay, FCM and recipients](../assets/diagrams/push-k-anonymity.webp) + +*Alice and Charlie share tag `0x75` for different channels, so both devices wake and each verifies locally; Bob's tag does not match, so his device never wakes.* + +**The 1-byte tag is the trick.** With 256 possible tags, many users share each one, which is the k-anonymity: the relay knows "someone in bucket 173 has mail", not who. The cost is that your device occasionally wakes for someone else's message and goes back to sleep. + +## What each party learns + +| Party | Learns | Does not learn | +|---|---|---| +| Relay | Tag bucket, aggregate frequency | Content, sender, sender's IP, which user you are | +| Google or Apple | That your device runs Pombo | Content, sender, your channels or contacts | +| Network observer | Wake-signal timing | Content, sender, recipient beyond the tag bucket | + +The residuals, stated plainly: platform push tokens inherently tell Google or Apple *that* you use Pombo. The relay stores your token keyed to your tags, so it can link the notifications one device receives over time, though not whose device it is. The wake-verification step shows the storage node which stream your device polls, and when. And wake signals travel unencrypted by design, since they must stay cheap and carry nothing sensitive: the subscribe restriction is network-level, so a determined observer running a modified node could watch their timing. The design assumes that, because even a full view yields only buckets and timestamps. Small user bases mean small anonymity sets. + +## Platform notes + +- **iOS**: push requires the PWA installed to the Home Screen, iOS 16.4 or newer. A Safari tab receives nothing. +- **Android**: Firebase Cloud Messaging. Battery optimizations can delay delivery by minutes; exempting Pombo helps. +- **Availability**: one community relay serves the network today. If it is down, push pauses and messages are unaffected, waiting on the network as usual. Anyone can [run a relay](../operators/run-a-relay.md). + +Even when everything is right, push is best-effort and delivery can lag from seconds to minutes. [Troubleshooting](../use/troubleshooting.md) has the checklist. + +## In-app notifications + +Notifications *inside* Pombo, such as channel invites, do not use the push system at all. They travel through your DM inbox, end-to-end encrypted like any DM. diff --git a/docs/protocol/overview.md b/docs/protocol/overview.md new file mode 100644 index 0000000..fb8f244 --- /dev/null +++ b/docs/protocol/overview.md @@ -0,0 +1,72 @@ +--- +id: overview +title: Architecture +description: What Pombo borrows, what it had to build on top, and why a social app with no backend needs all of it. +--- + +# Architecture + +Pombo has no message backend. No server receives, routes or authorizes your conversations. Everything you would expect a backend to do is done by the client on your device or by open infrastructure anyone can run. + +``` +┌─────────────────────────────────────────────┐ +│ Pombo client (browser PWA / Android app) │ +│ identity · encryption · access · UI │ +├─────────────────────────────────────────────┤ +│ Streamr Network → message transport │ +│ (P2P pub/sub) & delivery │ +├─────────────────────────────────────────────┤ +│ Storage nodes → message history │ +│ (Streamr + Cassandra) & offline delivery │ +├─────────────────────────────────────────────┤ +│ Polygon PoS → ownership, │ +│ (registries + gates) access & payment │ +└─────────────────────────────────────────────┘ +``` + +## What Pombo composes + +Each layer is something that already existed. None of them was built for group chat, and none of them does the job alone. What makes Pombo a protocol rather than an integration is the part in the third column. + +| Borrowed | What it gives | What had to be built on top | +|---|---|---| +| **Streamr Network** | Pub/sub transport with signed messages and no accounts | Its key exchange needs the publisher online to hand keys out, which a chat app cannot assume. Pombo turns that layer off and distributes a channel key between members instead | +| **Streamr registries on Polygon** | Stream ownership and permissions as public state nobody can confiscate | Permissions are per address, so a channel of a thousand people would be a thousand transactions. Pombo puts one contract in front of the stream and makes membership contract state | +| **ERC-1271** | A way for a contract to vouch for a signature | Used here so the channel's gate *is* the publisher of record: the network checks membership on every single message, and adding a member costs no stream transaction at all | +| **Storage nodes** | History, so people who were offline catch up | `pombo-storage-node` ships Cassandra and one-step clustering, closes the metadata leak with signed reads, deletes on request, and validates at ingest | +| **Web Push and FCM** | A way to wake a device that is not running | A relay that knows whom to wake knows your social graph. Pombo sends k-anonymous tags with a proof-of-work cost, and the device works out locally whether the tag was for it | +| **ENS** | Human-readable names for addresses | Every reverse lookup tells an RPC operator whom you are talking to, so Pombo fires decoy lookups alongside the real one | + +Read the third column as a list: **the parts that usually live in a server were rebuilt as protocol**, and each one had to survive the absence of any coordinator. + +## What the client does that a server usually does + +| Job | Where it happens | +|---|---| +| Deciding who may read and write | A contract read, on every message, by the network itself | +| Distributing the keys that open a channel | Member to member over the channel's keys stream, answered by whoever is online | +| Moderating | Signed state on the channel's own streams, plus a signed deletion request to the storage nodes | +| Keeping your devices in step | Snapshots encrypted to yourself, in your own inbox | +| Notifying you | A tag your own device resolves after the fact | +| Charging for access | A transfer from the subscriber's wallet to the owner's, with nothing in between | + +None of these has a privileged party. That is the property the design exists to preserve, and what makes each job harder than its centralized equivalent. + +## Follow one message + +You type in a channel. + +1. The client encrypts it if the channel calls for it, signs it, and publishes it to one of the channel's [streams](streams.md). +2. It propagates directly between subscribers on the Streamr Network. +3. A storage provider archiving that stream checks the gate accepted the publisher, then keeps a copy. That copy is what members who were offline read later. +4. Why the network accepted it at all, who owns the channel and who may publish, is a record on Polygon PoS. + +Two deliberate choices sit inside step 1. The Streamr SDK's own encryption is **disabled**, so all confidentiality is the client's, covered in [Encryption](encryption.md). And the publisher on the wire is never your account, covered in [Publisher identity](identity.md). + +The client also talks to public RPC endpoints, The Graph, the [push relay](notifications.md) and the Explore curation manifest. None of them handles message content; what each one sees is in the [threat model](../security/threat-model.md). + +## The interface is replaceable + +Pombo, the app at app.pombo.cc, is *an* interface to this protocol, not *the* system. Identities, channels, permissions and history live on public networks, so anyone can build another client against the same streams. + +The same holds one layer down. `pombo-storage-node` is a fork of the Streamr node, and every capability it adds is announced over `/capabilities`, detected by the client, and optional by construction. diff --git a/docs/protocol/storage-and-sync.md b/docs/protocol/storage-and-sync.md new file mode 100644 index 0000000..116aa77 --- /dev/null +++ b/docs/protocol/storage-and-sync.md @@ -0,0 +1,69 @@ +--- +id: storage-and-sync +title: Storage and sync +description: How history persists, who may read it, what lives on your device, and how devices converge without a sync server. +--- + +# Storage and sync + +The Streamr Network delivers messages live; it does not remember them. Persistence comes from **storage nodes**: Streamr nodes that archive stream history and serve it back over HTTPS. + +Pombo publishes and runs its own node software, [pombo-storage-node](https://github.com/Pombo-app/pombo-storage-node). This page describes channels stored on it. + +:::note[Any Streamr storage node works] +A channel can point at a plain Streamr storage node and keeps working: content stays encrypted and history is served. What a plain node lacks is everything this page attributes to the node itself: validation at ingest, signed reads, erasure and the receipt timestamp. A channel's storage panel shows which of its providers support what. +::: + +## Storage providers + +What a channel points at is a **provider**: one identity registered on-chain, which may be a single machine or a cluster of servers sharing a replicated database. Everything a channel owner decides is per provider, not per machine. + +| | What it is | How to get it | +|---|---|---| +| **Pombo** | The cluster the Pombo project runs | Default, nothing to do | +| **Third-party** | Any provider someone else runs | Paste its address into the channel's storage panel | +| **Self-hosted** | Your own | [Run one](../operators/run-a-storage-node.md), register it, paste its address | + +All three are the same mechanism, with no privileged path for Pombo's cluster. A provider must publish an HTTPS endpoint on a real hostname, because a browser cannot read from anything else, and the app checks that before accepting it. + +A channel can hold several providers at once, added or removed on-chain at any point, so its history can have independent copies under different operators. Reads rotate across every healthy endpoint a provider publishes, and one that keeps failing drops out of the rotation for the session. + +What providers hold is what the network carried: ciphertext for protected channels, contract-backed channels and DMs, signed plaintext for open channels. + +## What the node checks + +**At ingest**, before storing a message, the node validates it the way a subscriber would: a valid signature, and publish permission for the publisher. A forged or unauthorized message is dropped, not stored. On the conversation stream of a read-only channel with visible authors, only the owner's and moderators' messages are stored. If the chain cannot be reached, the message is stored anyway, because an outage must not erase legitimate history. + +**On read**, the node asks who is making the request: + +- **Contract-backed channels**: the request is signed with your account, and the node serves it only if the gate accepts you at that moment. An expired subscription or a sold token stops the history there. If the node cannot reach the chain to decide, it refuses rather than guesses. +- **DM inboxes**: the same, with the owner as the only one who qualifies. +- **Open and protected channels**, and every channel's admin stream, are served to anyone. The admin stream is what lets a stranger see a channel's name and image before joining. + +Signing a read keeps everyone else out of a private channel's history. It also gives the provider a verified record of which private channels you open and when, instead of an inference from your IP. Choosing a provider is choosing who holds that record. + +**A receipt timestamp.** The node stamps each message with the time it received it. That stamp is the node's, not the publisher's, which is what lets a client tell a message that genuinely arrived last year from one that merely claims to. + +**Erasure.** The channel owner, a moderator, or a message's own author while they still hold its signing key can delete specific messages from the node before retention ends. Who can erase what is in [Moderation](moderation.md). + +## Retention + +Owners choose how long history is kept, per stream, from 1 to 365 days with a default of 180. The node prunes expired data on its own timer. A DM inbox has the same setting, chosen by its owner. + +Artifacts that are written once and then only read, the moderation snapshot, the channel image and the password challenge, are republished by the owner before their retention expires, so bans and pins do not quietly vanish. + +## What lives on your device + +- Your key and app state, contacts and channel list and settings, encrypted at rest and isolated per account. +- **Channel messages are not cached**: they are re-fetched from storage each time you open a channel. +- **Sent DMs are kept locally.** What you sent lives in the recipient's inbox, so your own copy is your record of it. + +## Cross-device sync + +Your DM inbox doubles as a personal sync channel. The app writes **self-encrypted snapshots** of your state to it. Another device with the same account pulls them, merges, and converges: contacts, channels, settings, sent messages and the keys of your private channels. Read state stays per device. + +Only your key decrypts a snapshot, and any payload not authored by your own wallet is rejected. The merge is designed so a channel you left cannot come back, which is also why restoring a backup cannot duplicate or resurrect anything. + +Three modes, under **Settings → Account → Device Sync**: Automatic, Skip on start, Manual only. + +A device offline for longer than your inbox's retention may miss intermediate snapshots; importing a [backup](../use/backup.md) resyncs it fully. diff --git a/docs/protocol/streams.md b/docs/protocol/streams.md new file mode 100644 index 0000000..4df129a --- /dev/null +++ b/docs/protocol/streams.md @@ -0,0 +1,65 @@ +--- +id: streams +title: Streams and partitions +description: The streams behind every channel and DM inbox, what each carries, and which are stored. +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Streams and partitions + +Every channel is a set of Streamr streams derived from one base ID: four for open and protected channels, five when membership is contract-backed. The stream ID has the form `{ownerAddress}/{id}`, which is what makes ownership self-evident. + +These are protocol constants, not settings. + +| Stream | Stored | Partitions | Carries | +|---|---|---|---| +| `…-1` message | Yes | 12 | Conversation (P0), edits and deletes (P1), moderator deltas (P2), file chunks (P3–P11) | +| `…-2` ephemeral | No | 3 | Presence and typing (P0), media signals (P1), media data (P2) | +| `…-3` admin | Yes | 3 | Moderation snapshot (P0), channel image (P1), password challenge (P2) | +| `…-4` keys | Yes | 4 | Key announcements (P0), requests and answers (P1), member roster (P2). Contract-backed only | +| `…-5` interactions | Yes | 3 | Reactions (P0), two reserved | + +## Who may publish where + +| Stream | Open and protected | Contract-backed, visible | Contract-backed, sealed | +|---|---|---|---| +| `-1` | Everyone, or the owner alone if read-only | The gate contract | The shared publish key | +| `-2` and `-5` | Everyone | The gate contract | The interactions key | +| `-3` | The owner | The owner | The owner | +| `-4` | None | Members | Members | + +The `-3` stays owner-only in every type, which is why the keys protocol cannot fold into it: any member must be able to publish a key request. + +## DM inboxes + +A personal inbox is a deterministic pair of streams derived from your address: a stored message stream of 13 partitions (messages, sync, sync blobs, notifications, then nine file-chunk partitions) and an ephemeral one for presence and typing. + +Its permissions are the inverse of a channel: **anyone publishes, only the owner subscribes**. The inbox metadata carries the owner's encryption public key, which is what lets anyone seal a message to them. + +## Diagrams + + + + +![Open channel: streams with their on-chain metadata, permissions and partition layout](../assets/diagrams/open-channel.webp) + +*Content flows unencrypted; the metadata variants cover listed/unlisted and read-only combinations. The interactions stream is not drawn here.* + + + + +![Protected channel: identical layout, with every partition encrypted by a key derived from the shared password](../assets/diagrams/protected-channel.webp) + +*Same layout, with every partition's content passing through AES-256-GCM under a PBKDF2-derived key. The interactions stream is not drawn here.* + + + + +![DM inbox: a stored inbox stream and an ephemeral one, with ECDH-derived AES-256-GCM on every partition](../assets/diagrams/dm-inbox.webp) + +*The inbox publishes the owner's encryption public key as on-chain metadata; every partition is sealed with a key derived via ECDH and HKDF before it touches the network.* + + + diff --git a/docs/reference/channel-types.md b/docs/reference/channel-types.md new file mode 100644 index 0000000..951fbc8 --- /dev/null +++ b/docs/reference/channel-types.md @@ -0,0 +1,42 @@ +--- +id: channel-types +title: Channel types +description: The five access models side by side, plus the two flags fixed at creation. +--- + +# Channel types + +Five access models, chosen at creation and never changed afterwards. + +
+ +| | Open | Protected | Closed | Gated | Paid | +|---|---|---|---|---|---| +| Who reads | Everyone | Password holders | Allowlisted addresses | Token or NFT holders | Active subscribers | +| Who writes | Everyone | Password holders | The same | The same | The same | +| Enforced by | Open by design | Client-side encryption | Gate contract | Gate contract | Gate contract | +| Content on the wire | Signed plaintext | AES-256-GCM ciphertext | AES-256-GCM under a channel key | Same | Same | +| Cost to join | Free | Free | Free, the owner pays gas to add you | Holding the asset | The subscription price | +| Moderators | No | No | Yes | Yes | Yes | +| Listed in Explore | Optional | Optional | Never | Optional | Optional | + +
+ +**Closed, gated and paid** are the contract-backed family: one [gate contract](../protocol/contract.md) per channel decides membership, and content is encrypted under a channel key only members obtain. They differ only in the rule the contract enforces. Which layer enforces reading, writing and history, and what happens when someone loses access, is in [How access works](../protocol/access.md). + +## Two flags fixed at creation + +**Identity on the wire**, contract-backed channels only: + +| | Sealed (default) | Visible | +|---|---|---| +| Publisher on the wire | The channel's shared publish key | The gate contract | +| Who can attribute a message | Members | Anyone, without a key | +| What an ex-member can still publish | Bytes that spend the channel's retention, until the owner resets the publish key. Clients drop them on arrival | Nothing, once the gate cache expires | + +**Read-only**: only the owner posts, plus the moderators in a contract-backed channel. Members read, react and show as online in every type. + +## Other axes + +- **Listed or Unlisted** decides whether the channel appears in Explore. Unlisted is the default and publishes no name or description on-chain. +- A channel can also be **write-only**, straight from the stream permissions: you publish, you do not read. diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md new file mode 100644 index 0000000..06ee304 --- /dev/null +++ b/docs/reference/glossary.md @@ -0,0 +1,69 @@ +--- +id: glossary +title: Glossary +description: The terms Pombo's documentation uses, defined once. +--- + +# Glossary + +**Account**: an Ethereum keypair generated on your device. Your address is your identity; there is no server-side record of you. + +**Admin stream**: the `-3`, where the owner publishes the moderation snapshot, the channel image and a protected channel's password challenge. Owner-only publish in every channel type, readable by anyone. + +**Channel**: Pombo's group space, a set of Streamr streams owned by its creator. Four streams, or five when membership is contract-backed. See [Streams and partitions](../protocol/streams.md). + +**Contract-backed channel**: closed, gated or paid. The three types whose membership is decided by a [gate contract](../protocol/contract.md) rather than by client convention. + +**Delta**: a moderator's signed moderation action, published on the message stream because only the owner may write to the admin stream. Applied on top of the owner's snapshot by every client until the owner confirms it. + +**DM inbox**: your personal mailbox, a stored stream anyone may publish to and only you may read, plus an ephemeral one. Also carries channel invites and cross-device sync. + +**Epoch key**: the content key shared by the members of a contract-backed channel. Versioned and rotated; each message names the key that sealed it. + +**Erase from storage**: deleting specific messages from a channel's storage providers, as opposed to hiding them in the apps. Irreversible, reported per provider. See [Moderation](../protocol/moderation.md). + +**Ephemeral stream**: the `-2`, carrying presence, typing and live media coordination. Never stored. + +**Explore**: the in-app discovery view, listing channels whose owners marked them Listed, filtered by a curation manifest the interface applies. + +**Gate contract, PomboGate**: one EIP-1167 clone per contract-backed channel, holding the access rule. See [The gate contract](../protocol/contract.md). + +**Identity on the wire**: the creation-time setting of a contract-backed channel. **Sealed** authors are attributable only by members; **Visible** authors by anyone. + +**Interactions key**: in a sealed channel, the shared key covering the ephemeral and interactions streams. Every member with access holds it, including those who cannot post. + +**Interactions stream**: the `-5`, carrying reactions, in every channel type. + +**Keys stream**: the `-4`, where key announcements, requests, answers and the member roster travel. Contract-backed channels only, and stored, which is what makes joining asynchronous. + +**Key responder**: a device an owner or moderator has marked to keep answering the channel's pending key requests while the app is open. + +**Listed, Unlisted**: whether a channel appears in Explore. Unlisted is the default and keeps the channel's name and description off-chain. + +**Message stream**: the `-1`, carrying conversation, edits and deletes, moderator deltas and file chunks. + +**Moderator**: an address the owner of a contract-backed channel appoints to moderate through signed deltas. Powers and limits in [Moderation](../protocol/moderation.md). + +**Channel pseudonym**: the throwaway keypair a client publishes under in a channel, created on first publish, never persisted, discarded when you leave. Your account travels as a signed proof inside the payload instead. + +**Publisher proof**: the signature by your account over your channel pseudonym, which is how other Pombo clients attribute a message published under it. + +**Read-only channel**: a channel where only the owner posts, plus the moderators in a contract-backed one. Members still read, react and show as online. + +**Relay**: the open-source server that turns Streamr wake signals into Web Push. It sees tag buckets and timing, never content or identities. + +**Roster**: one entry per member per epoch on the keys stream, sealed under the channel key, which is how members see who else is in. + +**Sealed sender**: a DM's property of hiding not only its content but its author. The network sees a throwaway publisher, and the sender's identity travels inside the encrypted envelope. + +**Shared publish key**: in a sealed channel, the key everything on the conversation stream is written under. Replacing it is the owner's **Reset Publish Key**. + +**Signed read**: a history request carrying the reader's address and a signature over it. Pombo storage nodes require one for contract-backed channels and DM inboxes, and check the gate before answering. + +**Storage node**: a Streamr node that archives stream history and serves it back over HTTPS. Pombo publishes and runs [its own node software](https://github.com/Pombo-app/pombo-storage-node); any Streamr storage node can also store a Pombo channel. + +**Storage provider**: one storage node identity as registered on-chain, which may be a cluster of several servers sharing a database. Capabilities and access decisions are per provider, and a channel can hold several. + +**Streamr Network**: the peer-to-peer pub/sub network Pombo uses for transport. Messages travel between subscribers with no relay server in between. + +**Wake signal**: the tagged, contentless message a sender broadcasts so the relay can wake a recipient's device. See [Notifications](../protocol/notifications.md). diff --git a/docs/reference/limits.md b/docs/reference/limits.md new file mode 100644 index 0000000..dba4084 --- /dev/null +++ b/docs/reference/limits.md @@ -0,0 +1,80 @@ +--- +id: limits +title: Limits and defaults +description: "Every number in one place: retention, file sizes, key rotation, push, crypto parameters, on-chain costs." +--- + +# Limits and defaults + +## Storage and retention + +| | Value | +|---|---| +| Retention range | 1 to 365 days, per stream | +| Default retention | 180 days | +| Where it is set | Channel settings, by the owner; your own settings for your DM inbox | +| Changing it | An on-chain transaction | +| Storage providers per channel | One or more, added or removed at any time | +| Messages per erase request | 100, batched automatically | +| Erasure reported as | Per storage provider, "erased on k of n" | +| Signed request validity | 5 minutes of clock skew, one nonce per request | +| Gate answers cached by the node | 10 minutes | +| Retention sweep on the node | Every 6 hours | + +## Files + +| | Value | +|---|---| +| Chunk size, storage transport | About 240 KB, the practical per-message limit | +| Chunk partitions | 9, round-robin | +| P2P transport, per file | 500 MB maximum | +| Seed cache | 700 MB, kept up to 7 days for re-serving | +| Download availability | Storage: the channel's retention. P2P: while a seeder is online | + +## Channel keys + +| | Value | +|---|---| +| Scheduled epoch rotation | Every 7 days, when an admin is present | +| Publish key reset, sealed channels | Two transactions | + +## Push notifications + +| | Value | +|---|---| +| Tag size | 1 byte, so 256 buckets shared by all users | +| Proof-of-work | 4 leading zeros, bound to a 10-second epoch | +| Registration refresh | Every 6 hours | +| Registration secrecy | Encrypted to the relay's public key | +| Content in the push path | None | + +## Cryptography + +| | Value | +|---|---| +| Channel and DM content | AES-256-GCM | +| DM key agreement | ECDH on secp256k1, fresh ephemeral key per message, HKDF-SHA256 | +| Protected-channel key | PBKDF2-SHA256, 310,000 iterations, from the password | +| Web app state at rest | AES-256-GCM under a key derived from a wallet signature | +| Web private key at rest | scrypt-encrypted keystore | +| Android key at rest | Encrypted preferences under a hardware-backed key | + +## On-chain costs + +Paid in POL on Polygon PoS, typically cents: + +- Creating a channel, and the gate contract if it is contract-backed +- Creating your DM inbox, once +- Adding or removing members of a closed channel +- Changing a channel's retention or storage providers +- Banning at the contract level, appointing a moderator, and resetting a sealed channel's publish key + +Chatting, DMs, file sharing, joining an open channel and rotating the channel key are free. + +## Platform requirements + +| | Value | +|---|---| +| Android | 8.0, API 26, or newer | +| iOS push | Requires the PWA installed to the Home Screen, iOS 16.4 or newer | +| Storage node endpoint | HTTPS on a real hostname | diff --git a/docs/security/client-security.md b/docs/security/client-security.md index fa96b40..5a986c8 100644 --- a/docs/security/client-security.md +++ b/docs/security/client-security.md @@ -1,7 +1,7 @@ --- id: client-security title: Client security -description: How the app itself is hardened — CSP, message sanitization, clickjacking defense, and keeping the key away from the UI. +description: "How the app itself is hardened: CSP, message sanitization, clickjacking defense, and keeping the key away from the UI." --- # Client security @@ -10,15 +10,15 @@ The [threat model](threat-model.md) describes what an attacker can achieve again ## No third-party code ever runs -The web app's Content Security Policy makes a single bet: **every script is bundled and served from the app's own origin**. External scripts, inline scripts and `eval` are refused, so compromising a CDN or injecting a `