Anonymity Is an Architecture Problem
The friends-online feature turned out to be one of the harder privacy decisions in the Nostr relay suite I’ve been working on. The server already has open WebSocket connections. Using them to show who’s online would be straightforward. It would also mean tying a person’s public key to a particular connection, which is information I don’t want the presence service to need.
That problem keeps coming up in different forms. A community platform needs to let people read, post, publish articles, sell things, and message each other. Eventually, it needs to handle payments. Each feature gives the server another reason to collect information about the person using it.
I’m designing this suite so someone can use it over Tor without having to give the platform their real-world identity. That means deciding what each service needs to know before building it, then checking that the implementation stays within those limits. A privacy policy can govern the handling of stored data. I’d rather avoid collecting data the service doesn’t need in the first place.
Keep signing keys on the client
Nostr uses a public key as an identity. Authentication proves control of the corresponding private key. It doesn’t require an email address, phone number, or ID document, and it doesn’t establish anything about the person holding the key.
For this suite, signing happens in the browser. The server receives a signed event, checks it, and forwards it. It has no reason to hold the user’s private key. That also removes the need for a conventional password-reset system and the personal information often collected to support account recovery.
There’s a build check behind that decision. A test scans the source tree for key-custody references and signing-curve imports outside two audited packages. It gives a future change that crosses that boundary somewhere concrete to fail in review and testing. The check has a specific scope; it doesn’t prove the whole system private. It does help keep server-side signing from slipping in as a convenience feature.
Presence without connection tracking
The design keeps Tor and clearnet ingress separate and avoids storing metadata that would correlate the two. Presence has to work within that constraint.
Deriving online status from WebSocket connections was the first approach I rejected. To answer whether Alice is online, the presence service would need to know which connection belonged to Alice. That association would be useful for more than displaying a status indicator.
Instead, presence uses a lease with a state and an expiry. The service checks the expiry when someone reads the status, so an expired lease stops being disclosed even if the cleanup job hasn’t run. Renewing a lease replaces the existing value rather than appending another entry to a login history.
The presence record has no IP address, Tor circuit, user agent, or device fingerprint. A friend can see that Alice is away without learning which device reported it or how that device reached the network.
Invisible status needs the same care. It should produce the same responses as offline status, including error behavior and timing. Otherwise, a caller may be able to detect a hidden session even when the interface says the user is offline.
Private messages still have metadata
Work on private messaging started with a bug. The legacy direct-message format names its recipient in plaintext, and those events were being served to any reader. The contents were encrypted, but someone could query the relay to learn which keys were exchanging messages.
Restricting event reads wasn’t enough. Aggregate counts needed restrictions too. If a stranger can count messages tagged with a particular key, they can still learn something about that key’s activity without reading a single message.
The newer messaging design uses gift wrapping to carry encrypted messages through the relay. That reduces what the relay can learn from the message itself, but it doesn’t remove the need to examine the outer event, routing information, and query permissions. Those details belong in the privacy review alongside the encryption.
Payments add another set of records
Payments and subscriptions are still planned work in this suite. Before implementing them, I want clear limits on the records they create and on how those records connect to a social identity.
The current design has three requirements:
- The platform doesn’t hold funds. It observes settlement through the payment system. A Nostr event claiming that someone paid is not sufficient proof.
- Financial keys stay separate from social keys. Static receiving addresses aren’t published by default. Reusing an address can link a pseudonymous social key to a public transaction history.
- Access credentials expire quickly. Each assertion applies to one resource and one audience, lasts minutes, and stays out of event logs and derived views. Access checks shouldn’t leave a lasting reading history behind them.
The threat model explicitly includes someone analyzing payment relationships. That matters when choosing a payment flow: keeping a name out of the checkout form won’t help much if the transaction records make the connection anyway.
Grant permission to write without asking for identity
Public keys are cheap to create. A relay can’t treat possession of a key as a reason to accept unlimited writes from it.
The suite calls its admission system “write trust.” It uses elapsed participation, bounded proof-of-work, behavioral history, community sponsorship, and optional economic friction to decide how much a key can write.
NEW ──▶ READ_ONLY_PROBATION ──▶ VERIFIED_WRITE_TRUST
└─▶ THROTTLEDA new key gets read access and a limited write budget during probation. Its allowance can grow based on its activity. None of that requires establishing who owns the key.
Subscriptions and write trust remain separate. Paying for content doesn’t buy permission to post more, and a trusted writer doesn’t automatically get paid content. Keeping those decisions separate also makes it easier to change an abuse rule without changing what a subscriber has purchased.
What’s built and what remains
As of this writing, the relays, projections, and abuse controls exist. Federation is partly built. Payments, subscriptions, and translation have design decisions behind them but aren’t implemented. The repository tracks those distinctions, and the threat model leaves unimplemented controls marked as such.
The presence decision is a useful reference for the remaining work. Using the connection table would have saved effort, but it would also have given another service access to an association it didn’t need. A lease takes more care to implement and has a narrower purpose.
That’s the work ahead: inspect what each feature asks the server to remember, keep only what’s necessary, and add checks where a later change could undo the decision. I’ll judge the design by what the finished services actually store and disclose.
Comments // nostr
no comments yet.
Reply from your Nostr client Reply on the web No zap needed. Replies appear here a few minutes after they reach the relay.