WhatsApp caps groups at 1,024 members. Signal at 1,000. Telegram supergroups stretch further by sharding. Most messengers cap somewhere in the low thousands because their servers have to fan out every group message to every member — duplicate the envelope, route it N times, deliver N pushes.
BlindPost works differently. For us, sending a group message is closer to publishing an article than to mailing letters. The author publishes once. Anyone who has the article's address — and the key to read it — can come and fetch it. The reader count has no theoretical limit, because the publishing step doesn't get any harder when more people read.
Our server holds the encrypted "article." Anyone holding the group's key (its members) can come fetch and decrypt. No fan-out, no per-member duplication, no membership list anywhere. Here's how that works at the standard-crypto level.
How a normal messenger handles a group message
When you post in a WhatsApp group, the server does roughly this:
- The message lands at the server, addressed to "group ABC."
- The server looks up the membership list for group ABC — let's say 1,000 entries.
- The server pushes a per-recipient envelope into each of the 1,000 members' delivery queues.
- Each member's phone receives one push.
This is server-side fanout. One incoming message becomes N outgoing pushes. The server's CPU, bandwidth, and storage scale with member count, multiplied across every message ever sent in every group ever created. Past a few thousand members the math turns ugly fast.
How a BlindPost group is built
A BlindPost group is, mechanically, just an X25519 public key. That's all. The thing our server stores in its "group" column is literally a base58-encoded public key.
When you create a group, your client:
- Generates an X25519 keypair locally.
- Uses the public half as the group's permanent identifier.
- Distributes the private half, encrypted end-to-end, to each invited member.
Membership, on BlindPost, is defined operationally: anyone who holds the group's private key. There is no list of members anywhere on our server. Members come and go by being added to or removed from the private-key circle, and our server has no way to enumerate that circle.
Two layers of identifier
Sharp readers may have noticed an awkward consequence of "a group is just an X25519 public key": if the key ever rotates (and we will, later in this post, to remove members), the cryptographic identifier changes too. From the server's point of view that's fine. From the user's point of view ("wait, did my group get a new ID?") it isn't.
So each BlindPost client maintains two layers of identifier for every group:
server_group_id— the cryptographic layer. Equal to the current X25519 public key. Rotates when the key does. Used for addressing messages and routing.local_group_id— a stable label, generated when the group is first created, that never changes. Used in your chat list, group settings, invite links — anywhere a user would think of "the group's identity." Your client privately maps this local label to whatever the cryptographic identifier currently is.
When we say "the group's public-key identifier" elsewhere in this post, we mean the cryptographic layer. The stable local label is purely for the UI to keep continuity when the underlying key changes.
How a group message travels
When you send a message to a group:
- Your client picks an ephemeral X25519 keypair, derives a shared secret via ECDH against the group's public key, encrypts the message once.
- Sends one envelope to the server, addressed to the group's public-key identifier.
- The server stores that envelope in the group channel. Done.
There is no fanout step. Your sender bandwidth is O(1) — you send the same single envelope whether the group has 5 members or 50,000.
How members read it: sparse pull
Members pull on their own schedule. Each member's client tracks a cursor — the last sequence number it read from the group channel — and periodically asks the server: "any new envelopes in group ABC since seq N?"
This is what we call sparse pull. Each client decides:
- When to check — active app in the foreground? Once a minute. Backgrounded? Less often. Phone off? Whenever the user comes back.
- What to pull — caught up enough? Skip. Five days behind? Grab the last 200. Brand new install? Maybe just the last 50 for immediate context.
- Whether to fetch older history at all — entirely the client's choice.
The server holds messages on disk, indexed by the group's public-key identifier. It serves whatever ranges it's asked for. It never tracks who's reading, who's a member, or who's behind. From the server's view, every fetch is just "some client requesting envelopes since seq N in group ABC." That client could be any of 100,000 humans, or one human pulling from five devices, or someone who just got hold of the public key out of curiosity.
The cost structure flips:
- Sender bandwidth: O(1) per message
- Server storage: O(messages) per group, independent of member count
- Server CPU: O(1) per push, O(1) per pull
- Receiver bandwidth: each member pulls only what they need, when they need it
"Member count" is a number that doesn't exist anywhere in our infrastructure.
A free privacy bonus
We didn't set out to hide group membership — we just don't have a place to write it down. The server has no membership table at all. It cannot answer "who is in group ABC" because the question has no data backing it.
The most a subpoena can extract from us is: "encrypted bytes flowed between IP A and group public-key B during this window." Whether A is a member of B's group, or just an automated client asking for envelopes by guessing public keys — we have no way to say.
The trade-offs
We pay for this in a few places:
- Members must pull. They only "receive" while their client is awake. We invest heavily in keeping the pull path efficient (synced-interval bookkeeping, gap detection, backfill on scroll).
- No server-side delivery receipts. The server can't tell you when 47 of 50 members have read a message — it has no idea who the 50 are. Read receipts are an end-to-end thing, client-to-client.
- No server-side moderation tools. Kick / mute / role management is signed signaling between members; the server can't help you enforce a ban, only relay it.
These trade-offs buy you a server that doesn't know your social graph, and group sizes limited by your phone's rendering performance — not our cluster's fanout capacity.
BlindPost