Username claiming is now handled automatically in verifyAuth() when a username doesn't exist. The separate claimUsername RPC method is no longer needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Rondevu Server
🌐 Simple WebRTC signaling with RPC interface
Scalable WebRTC signaling server with cryptographic username claiming, service publishing with semantic versioning, and efficient offer/answer exchange via JSON-RPC interface.
Related repositories:
- @xtr-dev/rondevu-client - TypeScript client library (npm)
- @xtr-dev/rondevu-server - HTTP signaling server (npm, live)
- @xtr-dev/rondevu-demo - Interactive demo (live)
Features
- RPC Interface: Single endpoint for all operations with batching support
- Username Claiming: Cryptographic username ownership with Ed25519 signatures (365-day validity, auto-renewed on use)
- Service Publishing: Service:version@username naming (e.g.,
chat:1.0.0@alice) - Service Discovery: Random and paginated discovery for finding services without knowing usernames
- Semantic Versioning: Compatible version matching (chat:1.0.0 matches any 1.x.x)
- Signature-Based Authentication: All authenticated requests use Ed25519 signatures
- Complete WebRTC Signaling: Offer/answer exchange and ICE candidate relay
- Batch Operations: Execute multiple operations in a single HTTP request
- Dual Storage: SQLite (Node.js/Docker) and Cloudflare D1 (Workers) backends
Architecture
Username Claiming → Service Publishing → Service Discovery → WebRTC Connection
alice claims "alice" with Ed25519 signature
↓
alice publishes chat:1.0.0@alice with offers
↓
bob queries chat:1.0.0@alice (direct) or chat:1.0.0 (discovery) → gets offer SDP
↓
bob posts answer SDP → WebRTC connection established
↓
ICE candidates exchanged via server relay
Quick Start
Node.js:
npm install && npm start
Docker:
docker build -t rondevu . && docker run -p 3000:3000 -e STORAGE_PATH=:memory: rondevu
Cloudflare Workers:
npx wrangler deploy
RPC Interface
All API calls are made to POST /rpc with JSON-RPC format.
Request Format
Single method call:
{
"method": "getUser",
"message": "getUser:alice:1733404800000",
"signature": "base64-encoded-signature",
"params": {
"username": "alice"
}
}
Batch calls:
[
{
"method": "getUser",
"message": "getUser:alice:1733404800000",
"signature": "base64-encoded-signature",
"params": { "username": "alice" }
},
{
"method": "claimUsername",
"message": "claim:bob:1733404800000",
"signature": "base64-encoded-signature",
"params": {
"username": "bob",
"publicKey": "base64-encoded-public-key"
}
}
]
Response Format
Single response:
{
"success": true,
"result": {
"username": "alice",
"available": false,
"claimedAt": 1733404800000,
"expiresAt": 1765027200000,
"publicKey": "base64-encoded-public-key"
}
}
Batch responses:
[
{
"success": true,
"result": { "username": "alice", "available": false }
},
{
"success": true,
"result": { "success": true, "username": "bob" }
}
]
Error response:
{
"success": false,
"error": "Username already claimed by different public key"
}
RPC Methods
getUser
Check username availability
Parameters:
username- Username to check
Message format: getUser:{username}:{timestamp} (no authentication required)
Example:
{
"method": "getUser",
"message": "getUser:alice:1733404800000",
"signature": "base64-signature",
"params": { "username": "alice" }
}
Response:
{
"success": true,
"result": {
"username": "alice",
"available": false,
"claimedAt": 1733404800000,
"expiresAt": 1765027200000,
"publicKey": "base64-encoded-public-key"
}
}
claimUsername
Claim a username with cryptographic proof
Parameters:
username- Username to claimpublicKey- Base64-encoded Ed25519 public key
Message format: claim:{username}:{timestamp}
Example:
{
"method": "claimUsername",
"message": "claim:alice:1733404800000",
"signature": "base64-signature",
"params": {
"username": "alice",
"publicKey": "base64-encoded-public-key"
}
}
Response:
{
"success": true,
"result": {
"success": true,
"username": "alice"
}
}
getService
Get service by FQN (direct lookup, random discovery, or paginated)
Parameters:
serviceFqn- Service FQN (e.g.,chat:1.0.0orchat:1.0.0@alice)limit- (optional) Number of results for paginated modeoffset- (optional) Offset for paginated mode
Message format: getService:{username}:{serviceFqn}:{timestamp}
Modes:
- Direct lookup (with @username): Returns specific user's service
- Random (without @username, no limit): Returns random service
- Paginated (without @username, with limit): Returns multiple services
Example:
{
"method": "getService",
"message": "getService:bob:chat:1.0.0:1733404800000",
"signature": "base64-signature",
"params": {
"serviceFqn": "chat:1.0.0@alice"
}
}
Response:
{
"success": true,
"result": {
"serviceId": "uuid",
"username": "alice",
"serviceFqn": "chat:1.0.0@alice",
"offerId": "offer-hash",
"sdp": "v=0...",
"createdAt": 1733404800000,
"expiresAt": 1733405100000
}
}
publishService
Publish a service with offers
Parameters:
serviceFqn- Service FQN with username (e.g.,chat:1.0.0@alice)offers- Array of offers, each withsdpfieldttl- (optional) Time to live in milliseconds
Message format: publishService:{username}:{serviceFqn}:{timestamp}
Example:
{
"method": "publishService",
"message": "publishService:alice:chat:1.0.0@alice:1733404800000",
"signature": "base64-signature",
"params": {
"serviceFqn": "chat:1.0.0@alice",
"offers": [
{ "sdp": "v=0..." },
{ "sdp": "v=0..." }
],
"ttl": 300000
}
}
Response:
{
"success": true,
"result": {
"serviceId": "uuid",
"username": "alice",
"serviceFqn": "chat:1.0.0@alice",
"offers": [
{
"offerId": "offer-hash-1",
"sdp": "v=0...",
"createdAt": 1733404800000,
"expiresAt": 1733405100000
}
],
"createdAt": 1733404800000,
"expiresAt": 1733405100000
}
}
deleteService
Delete a service
Parameters:
serviceFqn- Service FQN with username
Message format: deleteService:{username}:{serviceFqn}:{timestamp}
Example:
{
"method": "deleteService",
"message": "deleteService:alice:chat:1.0.0@alice:1733404800000",
"signature": "base64-signature",
"params": {
"serviceFqn": "chat:1.0.0@alice"
}
}
Response:
{
"success": true,
"result": { "success": true }
}
answerOffer
Answer a specific offer
Parameters:
serviceFqn- Service FQNofferId- Offer IDsdp- Answer SDP
Message format: answerOffer:{username}:{offerId}:{timestamp}
Example:
{
"method": "answerOffer",
"message": "answerOffer:bob:offer-hash:1733404800000",
"signature": "base64-signature",
"params": {
"serviceFqn": "chat:1.0.0@alice",
"offerId": "offer-hash",
"sdp": "v=0..."
}
}
Response:
{
"success": true,
"result": {
"success": true,
"offerId": "offer-hash"
}
}
getOfferAnswer
Get answer for an offer (offerer polls this)
Parameters:
serviceFqn- Service FQNofferId- Offer ID
Message format: getOfferAnswer:{username}:{offerId}:{timestamp}
Example:
{
"method": "getOfferAnswer",
"message": "getOfferAnswer:alice:offer-hash:1733404800000",
"signature": "base64-signature",
"params": {
"serviceFqn": "chat:1.0.0@alice",
"offerId": "offer-hash"
}
}
Response:
{
"success": true,
"result": {
"sdp": "v=0...",
"offerId": "offer-hash",
"answererId": "bob",
"answeredAt": 1733404800000
}
}
poll
Combined polling for answers and ICE candidates
Parameters:
since- (optional) Timestamp to get only new data
Message format: poll:{username}:{timestamp}
Example:
{
"method": "poll",
"message": "poll:alice:1733404800000",
"signature": "base64-signature",
"params": {
"since": 1733404800000
}
}
Response:
{
"success": true,
"result": {
"answers": [
{
"offerId": "offer-hash",
"serviceId": "service-uuid",
"answererId": "bob",
"sdp": "v=0...",
"answeredAt": 1733404800000
}
],
"iceCandidates": {
"offer-hash": [
{
"candidate": { "candidate": "...", "sdpMid": "0", "sdpMLineIndex": 0 },
"role": "answerer",
"username": "bob",
"createdAt": 1733404800000
}
]
}
}
}
addIceCandidates
Add ICE candidates to an offer
Parameters:
serviceFqn- Service FQNofferId- Offer IDcandidates- Array of ICE candidates
Message format: addIceCandidates:{username}:{offerId}:{timestamp}
Example:
{
"method": "addIceCandidates",
"message": "addIceCandidates:alice:offer-hash:1733404800000",
"signature": "base64-signature",
"params": {
"serviceFqn": "chat:1.0.0@alice",
"offerId": "offer-hash",
"candidates": [
{
"candidate": "candidate:...",
"sdpMid": "0",
"sdpMLineIndex": 0
}
]
}
}
Response:
{
"success": true,
"result": {
"count": 1,
"offerId": "offer-hash"
}
}
getIceCandidates
Get ICE candidates for an offer
Parameters:
serviceFqn- Service FQNofferId- Offer IDsince- (optional) Timestamp to get only new candidates
Message format: getIceCandidates:{username}:{offerId}:{timestamp}
Example:
{
"method": "getIceCandidates",
"message": "getIceCandidates:alice:offer-hash:1733404800000",
"signature": "base64-signature",
"params": {
"serviceFqn": "chat:1.0.0@alice",
"offerId": "offer-hash",
"since": 1733404800000
}
}
Response:
{
"success": true,
"result": {
"candidates": [
{
"candidate": {
"candidate": "candidate:...",
"sdpMid": "0",
"sdpMLineIndex": 0
},
"createdAt": 1733404800000
}
],
"offerId": "offer-hash"
}
}
Configuration
Environment variables:
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Server port (Node.js/Docker) |
CORS_ORIGINS |
* |
Comma-separated allowed origins |
STORAGE_PATH |
./rondevu.db |
SQLite database path (use :memory: for in-memory) |
VERSION |
0.5.0 |
Server version (semver) |
OFFER_DEFAULT_TTL |
60000 |
Default offer TTL in ms (1 minute) |
OFFER_MIN_TTL |
60000 |
Minimum offer TTL in ms (1 minute) |
OFFER_MAX_TTL |
86400000 |
Maximum offer TTL in ms (24 hours) |
CLEANUP_INTERVAL |
60000 |
Cleanup interval in ms (1 minute) |
MAX_OFFERS_PER_REQUEST |
100 |
Maximum offers per create request |
Database Schema
usernames
username(PK): Claimed usernamepublic_key: Ed25519 public key (base64)claimed_at: Claim timestampexpires_at: Expiry timestamp (365 days)last_used: Last activity timestampmetadata: Optional JSON metadata
services
id(PK): Service ID (UUID)username(FK): Owner usernameservice_fqn: Fully qualified name (chat:1.0.0@alice)service_name: Service name component (chat)version: Version component (1.0.0)created_at,expires_at: Timestamps- UNIQUE constraint on (service_name, version, username)
offers
id(PK): Offer ID (hash of SDP)username(FK): Owner usernameservice_id(FK): Link to serviceservice_fqn: Denormalized service FQNsdp: WebRTC offer SDPanswerer_username: Username of answerer (null until answered)answer_sdp: WebRTC answer SDP (null until answered)answered_at: Timestamp when answeredcreated_at,expires_at,last_seen: Timestamps
ice_candidates
id(PK): Auto-increment IDoffer_id(FK): Link to offerusername: Username who sent the candidaterole: 'offerer' or 'answerer'candidate: JSON-encoded candidatecreated_at: Timestamp
Security
Ed25519 Signature Authentication
All authenticated requests require:
- message: Signed message with format-specific structure
- signature: Base64-encoded Ed25519 signature of the message
- Username is extracted from the message
Username Claiming
- Algorithm: Ed25519 signatures
- Message Format:
claim:{username}:{timestamp} - Replay Protection: Timestamp must be within 5 minutes
- Key Management: Private keys never leave the client
- Validity: 365 days, auto-renewed on use
Anonymous Users
- Format:
anon-{timestamp}-{random}(e.g.,anon-lx2w34-a3f501) - Generation: Can be generated by client for testing
- Behavior: Same as regular usernames, must be explicitly claimed like any username
Service Publishing
- Ownership Verification: Every publish requires username signature
- Message Format:
publishService:{username}:{serviceFqn}:{timestamp} - Auto-Renewal: Publishing a service extends username expiry
ICE Candidate Filtering
- Server filters candidates by role to prevent peers from receiving their own candidates
- Offerers receive only answerer candidates
- Answerers receive only offerer candidates
Migration from v0.4.x
See MIGRATION.md for detailed migration guide.
Key Changes:
- Moved from REST API to RPC interface with single
/rpcendpoint - All methods now use POST with JSON body
- Batch operations supported
- Authentication is per-method instead of per-endpoint middleware
License
MIT