Skip to content

Nostr-based voice calling for rider/driver (via NostrCall) #77

@variablefate

Description

@variablefate

Goal (user-facing)

In-app voice calling between rider and driver, signaled and connected over Nostr. Use case: driver got lost finding the pickup, rider can't spot the driver at a busy curb, anything where text chat is too slow and an out-of-band phone call would otherwise require sharing real phone numbers — which the trusted-driver-but-not-PII model is built specifically to avoid.

This issue covers both Ridestr (rider Android) and Drivestr (driver Android) consumer-side integration of NostrCall, since they live in the same monorepo and consume the same SDK with mirrored authorization predicates.

Protocol home

The calling protocol is being designed as a standalone, publishable, cross-platform library:

github.com/variablefate/NostrCall — Rust core + UniFFI bindings, Swift shell for iOS, Kotlin shell for Android.

The full spec lives in SPEC.md. High-level architecture:

  • Nostr for signaling (call invite/response/hangup via NIP-17 gift-wraps, three new event kinds)
  • Hyperswarm DHT + holepunch for NAT traversal — no TURN servers, no signaling server
  • WebRTC for media (Opus voice in v0; voice + optional video v0.5+)
  • DTLS fingerprint binding signed in the Nostr-signed call offer, so a relay or DHT-level MITM cannot substitute its own media keys

Call authentication runs entirely through the existing Roadflare/Ridestr key-sharing scheme — gift-wraps are signed by the caller's Nostr identity and the canReceiveCall predicate enforces "caller is in my followed-and-key-shared list" against existing key state. No FCM, APNs, or third-party identity layer is involved in authentication.

This issue tracks the ridestr/drivestr consumer-side integration work — wiring up NostrCall once it's ready. The protocol design itself moves to that repo.

v0 lifecycle constraint: both apps in foreground

NostrCall v0 deliberately scopes itself to calls placed while both peers have the app in the foreground. This keeps v0 truly zero-infrastructure — no push notifier of any kind, no Apple/Google credentials anywhere. The Nostr subscription for call invites runs over the live WebSocket while the app is open; calls placed to a backgrounded or killed app surface as a missed-call indicator next time the user opens it.

Android is technically capable of always-on background subscriptions via a foreground service, but adopting that pattern for v0 would make the cross-platform UX asymmetric with the iOS sibling (roadflare-ios). Both platforms ship with the same foreground-only constraint for v0. If demand for backgrounded-receive surfaces post-launch, NostrCall Phase 9 adds the Android-specific pieces (ConnectionService + foreground service + FCM data-only push) symmetrically with the iOS pieces; the wire protocol does not change.

See SPEC.md §8.0 and §8.4` for the architectural rationale.

Consumer-side scope

When NostrCall reaches Phase 5 (v0 in-app call UX shipped on both platforms), integration in this monorepo is:

Ridestr (rider Android)

  • Add NostrCall Kotlin module as a dependency
  • Define canReceiveCall predicate — "caller is a followed driver AND has shared a roadflare-key with us"
  • Embed NostrCallRinging(call) composable at the root activity layer (or implement custom UI driven by call.incomingCalls Flow)
  • AndroidManifest: <uses-permission android:name="android.permission.RECORD_AUDIO" /> and <uses-permission android:name="android.permission.INTERNET" />
  • Microphone permission flow on first call attempt (request rationale dialog if denied once)
  • AudioFocus management handled inside NostrCall's Android shell; consumer just doesn't fight it
  • "Call driver" affordance on the active-ride and chat surfaces; hidden when no active ride or key not yet shared
  • Missed-call indicator surface — design call (Drivers list? recent rides? chat threads?)

Drivestr (driver Android)

  • Same NostrCall Kotlin module dependency
  • canReceiveCall predicate mirrored — "caller is a followed rider AND has shared a roadflare-key with us"
  • Same Compose ringing UI integration
  • Same AndroidManifest permissions
  • "Call rider" affordance on the in-progress-ride surface; hidden during pre-acceptance / post-completion
  • Missed-call indicator surface on the driver side
  • Verify driver-mode lifecycle: foreground-service / DriverModeActivity interaction doesn't break the call subscription

What's deferred from v0

  • Video. Voice-only for the initial ship. Protocol supports it; we'll add it when a real use case appears.
  • Group calls. Pairwise only.
  • Call recording / transcription. Out of scope.
  • Backgrounded-receive ringing. v0 only rings when the app is foreground. Calls to a backgrounded/killed app surface as missed-call indicators. NostrCall Phase 9 path exists; expand only on real user demand.
  • Multi-device ringing. v0 rings the device that processed the gift-wrap first.
  • System Phone-app integration. No ConnectionService in v0; in-app UI only.

Dependencies / ordering

  • NostrCall Phase 0-5 must land before this issue is implementable. Track in the NostrCall repo's SPEC.md §12.
  • roadflare-ios#82 is the iOS sibling — same NostrCall SDK consumed from the Swift side. Once both ship Phase 5 integration, cross-platform calls (iOS rider ↔ Android driver, and Android rider ↔ iOS driver) work natively.

Status

  • 🟢 Protocol spec drafted (NostrCall repo, May 2026)
  • 🟢 v0 scoped to foreground-only (no push infrastructure required)
  • 🟡 NostrCall implementation: not started; Phase 0 feasibility spike pending
  • 🟡 ridestr/drivestr integration work: blocked on NostrCall Phase 5

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions