If you have not met ntfy, the pitch is one sentence: pick a topic name, POST to it, subscribe to it, done. No accounts, no setup, no SDK. You can push a notification to your phone from a shell script in the time it takes to read this paragraph. I use it constantly, and that effortlessness is the whole point. But there is a catch hiding in the simplicity, and once you see it you cannot unsee it: the topic name is the only thing protecting your data. Anyone who knows or guesses the name can read and write the topic. There is no notion of "this channel belongs to these two parties and nobody else." For alerts that is fine. For anything you would mind a stranger reading, it is not. doublethink is my attempt to keep ntfy's ease and add exactly the thing ntfy deliberately leaves out: genuinely private channels.
The first decision was whether to build anything at all. There are many secure message brokers. So before writing a line of code I did the honest prior-art pass: does something already sit at the exact intersection I wanted, which is ntfy-level setup friction and real privacy? I looked at ntfy's own access-control features, at MQTT brokers with ACLs, at NATS with its accounts and signed JWTs, at managed services like Pusher and Ably, and at Matrix as an end-to-end-encrypted substrate. The finding was clean and a little surprising: the two properties trade off against each other in every existing self-hostable tool. The ones that are ntfy-easy are open by default. The ones with real per-channel authorization, NATS especially, are genuinely good at it but cost far more setup ceremony than ntfy. And almost none of the self-hostable ones encrypt so that the operator cannot read the messages. The managed services that do offer end-to-end encryption are closed and hosted-only. So the gap was real: a self-hosted broker that is ntfy-easy and keeps the operator out of your private traffic did not exist. That is the cell doublethink fills.
My first design was wrong, and it is worth saying why, because the mistake is a common one. I reached for the heavy, "obviously secure" machinery: every party would have a cryptographic device keypair, the broker would authenticate connections with a signed challenge, and pairing two parties would involve a one-time invite code plus a short authentication string that both humans compare out of band to rule out a man-in-the-middle. It was, on paper, the strong design. I built it. Then I tried to use it and realized I had quietly betrayed the entire premise. Every private channel now required a multi-step ceremony with an operator in the loop. The thing that made ntfy worth imitating, that you can stand it up and point things at it in minutes, was gone. I had built a secure broker that was no longer ntfy-easy, which is to say I had built one of the things that already existed.
So I deleted it. The replacement is almost embarrassingly simpler, and that simplicity is the feature. A private channel is gated by one high-entropy shared secret. You create a channel with a single request and get the secret back. You hand that secret to the other party over any trusted path you already have. Whoever holds the secret can join the channel; nobody else can. It is the ntfy mental model exactly, pick a thing and share it, except the thing is an unguessable secret rather than a human-readable name, and that one change is what closes the hole.
The part that matters is how the secret is used, because it has to do two different jobs without letting the broker do a third. From the secret, each side derives two independent keys with a key-derivation function. One key authenticates you to the broker: the broker stores it and admits you by a challenge-response, so it can check that you hold the secret without the secret ever crossing the wire. The other key encrypts your messages, and it is derived with a different label, so the broker holding the first key cannot compute the second. The broker therefore knows you are allowed on the channel and faithfully relays your messages, but the messages themselves are end-to-end encrypted between the parties who share the secret. The operator of the broker, me, on my own server, cannot read them. That property, the operator cannot read private traffic, is the entire reason the project exists, and I was careful to have it cross-checked rather than trust my own first instinct on the crypto.
I want to be precise about what that does and does not buy you, because overselling encryption is its own kind of dishonesty. doublethink is payload-blind, not metadata-blind. The broker cannot read your message contents, but it does see the channel id, the message timing, and the sizes. The model is also symmetric: both holders of the secret can send and read, so a message proves a holder sent it, not which holder. These are deliberate, documented limits, not things I am hiding in a footnote. For "two parties who already trust each other and want a private pipe between them," which is the actual use case, they are the right trade.
Here is where it stops being theory. The first thing I use doublethink for is debugging Android builds. Anyone who has chased a bug that only reproduces on a real device, on a flaky network, away from the debugger, knows the pain: adb logcat is not attached, the crash is gone, and you are reduced to guessing. So I have my in-development APK POST its debug stream, structured logs, the state at the moment something goes wrong, a stack trace, to a private doublethink channel, and I subscribe to that channel on my laptop. The phone can be on cellular on the other side of the city. The debug info shows up on my screen in real time, it is end-to-end encrypted so the device's findings and whatever they contain are not sitting in cleartext on a relay, and the test build only needs the channel secret baked in, not a whole logging backend. When the bug reproduces, I have the evidence instead of a shrug.
The second use is wiring tools and agents together. A lot of what I build now involves separate processes that need to talk: a local agent and a browser front end, a long-running task that streams progress, two CLIs that want to hand work back and forth. The channel is bidirectional, asynchronous, and streaming, so a background job can emit many messages over time and the other side receives them as they arrive rather than in one batch at the end. It is the same shape as ntfy, which is exactly why it is pleasant to point a tool at, but I can put real instructions and real output through it without the uneasy feeling that the topic name is the only thing between my data and a stranger.
Going public forced one more round of honesty. The moment a broker is reachable by anyone, "anyone can create a channel" stops being a charming ntfy trait and becomes an open relay you have handed to the internet. So the latest version adds the unglamorous machinery a public instance actually needs: lightweight accounts with an API key for anyone who wants persistence, opt-in message retention so a peer that was offline can reconnect and catch up on exactly what it missed, a time-to-live that ages old messages out, per-channel and per-account storage quotas so nobody can fill the disk, and rate limits on the noisy paths. Ephemeral channels stay anonymous and zero-ceremony, the ntfy path; retained channels, which cost storage, require an account so the cost can be attributed. There is also an operator key, set in an environment variable, that lets me raise limits for a channel I care about without giving anyone a way to read its contents.
Then a real user asked for something the design did not yet do, and it pushed the project somewhere I like better. The setup: a small webring wanted to let visitors create their own persistent topics from a page, a guestbook, essentially. Two problems fell out of that. First, creating a topic that never expires is a privileged act, you do not want anyone on the internet minting permanent storage on your disk, so it needs an operator's blessing. But, second, the operator must not learn the channel's secret, or the privacy promise is a lie. The answer is a grant ticket. The operator, holding the admin key, issues a single-use, time-limited ticket that authorizes one permanent, capped channel under a fixed namespace. The user redeems that ticket while creating the channel with their own secret. So the operator authorizes the durable channel without ever seeing the secret and still cannot read it; the policy comes from the ticket, never from the client, so a leaked ticket cannot widen its own permissions. The admin key lives only on the server, never in the browser, which meant standing up a tiny separate service to broker those grants, the one place the key is allowed to be.
Building that surfaced a sharper question the user asked plainly: a guestbook is public, so why pay for end-to-end encryption at all? They were right. So the grant became encryption-agnostic, and the choice moved to the person who owns the data, where it belongs. Redeem a ticket with a key and you get the encrypted channel as before. Redeem it without one and you get a plaintext permanent topic, reachable on the open ntfy-style path with no key ceremony: anyone can post, anyone can read, and a fresh visitor catches up on the whole backlog. That last kind deliberately gives up the "operator cannot read it" property, because for genuinely public data that property buys you nothing and costs you key management. The honest thing was to make that a visible, documented choice the user makes at creation time, not a default I picked for them. Somewhere in here I also swapped the storage engine to Redis, because the load is high-throughput rather than high-volume, and wrote down exactly how much a hard crash can lose (about a second), because "permanent" should mean the data does not age out, not that I am promising more durability than the disk gives.
None of that last paragraph is exciting, and that is the point I keep relearning. The interesting idea in doublethink, ntfy-ease plus a shared secret that the broker cannot decrypt, fits in a sentence. The work that makes it something you can actually trust with real traffic on the open internet is in the boring parts: the quota that is enforced inside a transaction so it cannot be raced, the retention that deletes on expiry instead of leaking storage, the secret scan that caught me about to commit a real admin key into a public repository as a test fixture. doublethink is open source, it runs in Docker or as a single binary, and it carries a blunt no-warranty notice, because a tool whose entire job is holding other people's private traffic should be honest about being the work of one person, reviewed but not infallible. If you have ever wanted ntfy's ease for something you would not want a stranger to read, that is the gap it is trying to fill. There is a live demo on the project page that runs entirely in your browser: it shows you the exact encrypted bytes the broker relays next to the plaintext only the secret-holder can recover, so you can watch the privacy work rather than take my word for it.
