Built for app developers. Engineered for privacy.
DropOnAir gives your app secure messaging primitives without forcing you to build and maintain messaging infrastructure.
End-to-end encryption by default
Messages are encrypted on the sender's device before they leave. The DropOnAir server relays encrypted blobs, it never has access to plaintext content.
- ✓ Client-side key generation per conversation
- ✓ Blind relay, server cannot read content
- ✓ Forward secrecy support
- ✓ Zero knowledge at rest
const { messageId } = await client.sendMessage('user-456', 'Hello!');
// Server only ever sees:
"eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSJ9..."
// Recipient gets plaintext via onMessage
client.onMessage(msg => console.log(msg.plaintext));
Message queued for offline user
Waiting for reconnection…
Delivered automatically on reconnect ✓
Offline message delivery
When a recipient is offline, messages are durably queued and pushed to their device the moment they reconnect, no polling, no missed messages.
- ✓ Configurable queue size per user
- ✓ FIFO delivery order preserved
- ✓ Works across mobile app restarts
- ✓ Configurable message retention window
Delivery receipts & read status
Know exactly what happened to every message. Receipts are pushed to the sender in real-time via the same WebSocket connection.
In local queue
Reached server
Recipient device
Opened by user
client.onEvent(event => {
const { messageId, type } = event;
// type: MESSAGE_DELIVERED | MESSAGE_READ
updateUI(messageId, type);
});
Voice & video calls, built in
Add real-time voice and video calling to your app in minutes. DropOnAir handles the signaling layer over the same encrypted WebSocket used for messages, you supply the WebRTC peer connection.
- ✓ Invite, ring, accept, reject, end, full lifecycle
- ✓ SDP offer/answer + ICE candidate relay
- ✓ Toggle video on/off mid-call
- ✓ Short-lived TURN credentials for NAT traversal
- ✓ Voice-only or video calls
const callId = await client.startCall('user-id');
// React to events
client.onCallEvent(event => {
if (event.type === 'CALL_ACCEPTED')
startWebRtcNegotiation(event.callId);
});
// End the call
await client.endCall(callId);
Same user, multiple devices
Each device has its own E2EE keypair. Messages are encrypted per-device and delivered everywhere simultaneously.
Seamless multi-device support
Users can log in from phone, tablet, and desktop at the same time. The SDK generates a unique keypair per device, encrypts a separate payload for each, and the server fans messages out to every active session.
- ✓ Per-device X25519 keypairs
- ✓ Transparent per-device encryption
- ✓ Server-side fan-out to all devices
- ✓ Backward compatible with single-device clients
- ✓ No client code changes required
Messages (ClearText)
Not every use case needs end-to-end encryption. Messages (ClearText) lets you skip key exchange and send plaintext over the same WebSocket, perfect for notifications, bot messages, support widgets, and public chat rooms.
- ✓ No key exchange or crypto setup required
- ✓ Same WebSocket transport as E2EE messages
- ✓ Offline delivery & delivery receipts included
- ✓ Works alongside E2EE, choose per message
- ✓ Available on all plans
await client.sendCleartextMessage(
'user-456',
'Welcome to the platform!'
);
// Receive, same onMessage callback
client.onMessage(msg => {
console.log(msg.plaintext);
});
Broadcast / Pub-Sub channels
Publish messages to named channels and deliver them to every subscriber in real time. Ideal for live feeds, announcements, collaborative features, and any one-to-many messaging pattern.
- ✓ Real-time fan-out via WebSocket
- ✓ Per-channel subscriber management
- ✓ Message history with auto-expiring TTL
- ✓ REST API for subscribe / unsubscribe
- ✓ Per-plan channel limits and quotas
await client.subscribeBroadcast('announcements');
// Listen for broadcasts
client.onBroadcast(msg => {
console.log(msg.channelId, msg.plaintext);
});
// Publish to a channel
await client.publishBroadcast(
'announcements',
'Version 2.0 is live!'
);
Group messaging
Create groups, add and remove members, and send E2EE or cleartext messages to all participants at once. The SDK handles sender-side fan-out with per-member encryption, the server never sees plaintext.
- ✓ E2EE group messages (sender-side fan-out)
- ✓ Cleartext group messages for public groups
- ✓ REST API for group CRUD + member management
- ✓ Per-plan limits on groups, members, and messages
- ✓ Offline delivery for group messages
const group = await client.createGroup(
'Project Chat',
['user-1', 'user-2']
);
// Send E2EE group message
await client.sendGroupMessage(
group.groupId, 'Hello team!'
);
// Listen for group messages
client.onGroupMessage(msg => {
console.log(msg.groupId, msg.plaintext);
});
Group voice & video calls
Start mesh WebRTC group calls with signaling handled over the same encrypted WebSocket. The SDK provides call lifecycle events, you render the UI.
- ✓ Mesh topology, each peer connects directly
- ✓ WebSocket-based call signaling (SDP/ICE)
- ✓ Per-plan participant limits (up to 32)
- ✓ Join / leave / end events for all participants
- ✓ Usage-metered group call minutes
await client.startGroupCall(
group.groupId, 'video'
);
// Listen for group call events
client.onGroupCallEvent(event => {
if (event.type === 'OFFER') {
// Handle incoming SDP offer
}
});
Open-source SDKs for every platform
Native SDKs for Android and iOS ship alongside the JavaScript SDK. Integration patterns stay consistent, same crypto, same proto wire format, same API shape.
First-class Android & iOS SDKs
Build truly native messaging apps with the same E2EE guarantees as the JS SDK. CryptoKit on iOS, BouncyCastle on Android, no third-party crypto wrappers needed.
- ✓ Swift Package Manager (SPM) distribution
- ✓ Gradle / Maven artifact for Android
- ✓ Keychain & Keystore secure key storage
- ✓ Same protobuf wire format across all platforms
- ✓ Cross-platform test vectors for E2EE verification
appId: "your-app-id",
publicApiKey: "your-key",
getUserJwt: { try await authService.getJwt() },
tokenExchangeEndpoint: "/api/droponair/token",
keyDirectoryEndpoint: "/api/droponair/keys"
))
appId = "your-app-id",
publicApiKey = "your-key",
getUserJwt = { authService.getJwt() }
))