Building on Nostr: Real Engineering Challenges

WebSocket Management, Relay Discovery, Data Consistency, and Key Management in a Decentralized Protocol

6 min read

This is Part 2 of a two-part series. Part 1 covers the protocol fundamentals — events, relays, NIPs, and the architecture that makes Nostr different.

Nostr’s design is deceptively simple: one data type, dumb relays, smart clients, cryptographic identity. But that simplicity means every problem a traditional server would solve — authentication, data consistency, connection management, content moderation — now lands on the client.

When our team joined the diVine project, we learned this quickly. The challenges we hit are not unique to video. They are structural, baked into what it means to build on a protocol where the server is deliberately dumb.

Building Nostr Engineering Challenges Hero

Managing WebSocket Connections

Your client maintains simultaneous WebSocket connections to multiple independent relays — often five to fifteen at once. Because relays do not talk to each other and different users publish to different relays, connecting to multiple relays is not optional.

Connection lifecycle is your responsibility. Each relay connection can drop independently. Your client needs reconnection logic with exponential backoff, health monitoring per connection, and graceful degradation. The practical approach is a dedicated connection manager layer between your app logic and raw WebSocket connections, owning lifecycle, subscription routing, and resource budgeting in one place.

Subscription management gets complex fast. Loading a feed requires subscriptions across multiple relays for followed accounts, replies, reactions, and profile metadata — multiplied by every screen in your app. Without careful lifecycle management, relays will reject new subscriptions or drop your connection. Tying subscription cleanup to your navigation lifecycle helps.

Battery and bandwidth matter. On mobile, dozens of concurrent WebSocket connections consume battery and data at measurable rates. Lazy connection is the most effective mitigation: only open a WebSocket when your app needs data from that relay, and close it when the relevant screen is no longer visible.

Local caching is load-bearing. No server exists to go back to. Without local persistence, every app launch re-fetches the user’s entire social graph from scratch. Treat the local cache as your primary data source — a structured database with indexes on event kind, pubkey, and created_at. Serve the UI from cache on launch, then merge incoming relay data in the background using a stale-while-revalidate pattern.

WebSocket Connection Management Complexity

Relay Discovery and the Data Puzzle

Managing connections is only half the problem. The harder question is: which relays should you connect to?

Fetching a timeline on Nostr means retrieving the user’s follow list, finding each followed user’s relay preferences (kind 10002), connecting to those relays, subscribing, and merging results. This is what’s called the “outbox model”. The problem is that many users have never published relay preferences. When preferences are missing, your client must fall back to querying popular relays (slow, bandwidth-heavy) or showing nothing (worse).

Most clients maintain default relays as a fallback, but the more you rely on defaults, the more your “decentralized” app resembles a centralized one. Prompting users to publish their own relay list early reduces this dependency over time.

Relay Discovery Network Puzzle

Relay Reliability

Relays are independent operators with their own policies, retention periods, and rate limits. They can go offline without notice. Publish events to at least three relays for redundancy, confirm acceptance before showing the user a success state, and build relay health tracking (response times, error rates) into your connection manager so you can prioritize reliable relays and deprioritize those that consistently fail.

Data Consistency Without a Server

Without a single source of truth, your client may receive different data from different relays. One relay has a user’s updated profile; another serves the old version. One relay has all the replies in a thread; another has a subset. This is not a bug — it is the structural cost of decentralization.

The local cache you built for performance now has a second job: reconciliation. Use created_at timestamps as the primary conflict resolution rule — when two relays return different versions of the same replaceable event, keep the later one. For threads and feeds, a time-bounded scatter-gather pattern works well: query all relevant relays in parallel, render the best available result after a short timeout, and continue merging late-arriving responses into the view. Design your cache to support upsert-on-newer logic natively and use subtle UI indicators to communicate ongoing sync without blocking the user.

Your client is an eventually consistent system. If your background is mobile apps that consume REST APIs, this is a significant shift.

Relay Data Consistency Reconciliation

Key Management: Where Users Hit a Wall

Nostr’s identity model is cryptographically elegant and practically painful. Every identity is a key pair — no email, no password reset, no support ticket. For engineers, the model is clean. For the users you are building for, it introduces friction at every stage.

Key Management Onboarding Trade-off

Onboarding Friction

A new user’s first interaction: generate a cryptographic key pair, understand that their private key must be backed up, and store it securely. Compare that to “Sign in with Google.”

Every client must decide how much to abstract. Auto-generate for fast onboarding and users may never back up their key; show the raw hex string and the funnel drops. One approach: auto-generate at first launch, then surface non-blocking backup prompts at natural pause points — after the first follow, the first post, a week of usage. These tradeoffs are invisible in the protocol spec but very real in the product budget.

Cross-Platform Identity

A user creates their identity on desktop and now wants to use your mobile app. The current options: copy-paste a hex string (insecure), scan a QR code (requires both devices in hand), or use an external signer app.

Bunker-based remote signing, where a separate service holds the private key and signs events on the client’s behalf, remains the clearest path forward. Remote signing solutions are maturing but remain friction-heavy for mainstream users.

Cross-Platform Identity Ecosystem Gap

UX Patterns You Have to Invent

Building on Nostr means designing interfaces for concepts that have no precedent in centralized apps. Users arrive expecting familiar patterns that do not exist — your UI must teach new ones.

Relay selection. Most users should never think about relays. Pick sensible defaults on first launch and expose relay management as an advanced setting. That balance between abstraction and transparency is what every client solves differently.

Identity switching. Some users operate multiple key pairs — a public persona, an anonymous account, a project identity. Switching means swapping the active key pair along with its cached state and relay configuration. Treating each identity as an isolated profile keeps the logic clean.

No delete button. The protocol defines deletion request events (kind 5), but relays are not required to honor them. “Delete” means “ask relays to delete.” Label the action honestly to set the right expectation.

Content filtering without a server. Every event arrives unfiltered. Your app decides what to show and what to hide. For consumer-facing apps, this means building a client-side moderation layer: keyword filters, mute lists, and community-curated block lists published as Nostr events.

The Ecosystem Today

Community-maintained libraries exist across most languages, and published packages cover the core functionality: key encoding, relay connections, event creation, and signing. The Dart/Flutter ecosystem is production-viable — we built diVine’s client layer on it. What you will not find yet is the depth of tooling around established platforms: standardized testing frameworks, comprehensive guides, or stable abstraction layers that insulate you from protocol changes.

The protocol evolves fast. No stable LTS version exists — just community consensus about which NIPs are mature. Start with the core NIPs (NIP-01 for events, NIP-02 for follows, NIP-65 for relay lists) and add newer NIPs incrementally as your feature set demands.

Nostr Developer Tooling Maturity Gap

None of these challenges are unsolvable. Libraries exist, documentation is improving, and the community is active. But the protocol is young and evolving fast, which means the ground shifts under you — a NIP you build against today may change next month. Keeping pace with the protocol through resources like Nostrbook MCP can make a real difference (check out our MCP blog). Budget for that pace, and the engineering is entirely manageable.