A pluggable TypeScript library for WebRTC communication between web and mobile applications, with built-in Firebase signaling support.
- 🔌 Pluggable Signaling Architecture - Abstract signaling interface with Firebase implementation included
- 🎯 Framework-Agnostic Core - Use with vanilla JS, React, Vue, Angular, or any framework
- ⚛️ React Hooks - Ready-to-use React hooks for seamless integration
- 📦 Tree-Shakeable - Only bundle what you use with ESM/CJS dual output
- 🔒 Full TypeScript - Strict typing with comprehensive type definitions
- 🔥 Firebase Ready - Built-in Firebase Realtime Database signaling
- 🧊 Trickle ICE - Efficient incremental ICE candidate exchange
- 🎨 Type-Safe Events - Strongly-typed event emitter for all connection events
npm install @grndd-systems/zk-proof-rtcDepending on your use case, you may need to install peer dependencies:
# For React hooks
npm install react
# For Firebase signaling
npm install firebaseimport { useWebRTCWithFirebase, generatePeerId } from '@grndd-systems/zk-proof-rtc';
function DesktopApp() {
const { state, createOffer, send, onMessage } = useWebRTCWithFirebase({
firebaseConfig: {
apiKey: 'YOUR_API_KEY',
authDomain: 'YOUR_AUTH_DOMAIN',
databaseURL: 'YOUR_DATABASE_URL',
projectId: 'YOUR_PROJECT_ID',
},
webrtcConfig: {
debug: true, // Enable logging
},
});
useEffect(() => {
onMessage((data) => {
console.log('Received:', data);
});
}, [onMessage]);
const handleConnect = async () => {
const peerId = generatePeerId();
await createOffer(peerId);
// Show QR code or share peerId with mobile app
console.log('Share this ID:', peerId);
};
return (
<div>
<button onClick={handleConnect} disabled={state.isConnected}>
Create Connection
</button>
<p>Status: {state.state}</p>
{state.isConnected && (
<button onClick={() => send({ type: 'greeting', message: 'Hello!' })}>
Send Message
</button>
)}
</div>
);
}import { useWebRTCWithFirebase } from '@grndd-systems/zk-proof-rtc';
function MobileApp({ peerId }: { peerId: string }) {
const { state, createAnswer, send, onMessage } = useWebRTCWithFirebase({
firebaseConfig: { /* same config */ },
});
useEffect(() => {
// Auto-connect when peerId is available
createAnswer(peerId);
}, [peerId, createAnswer]);
useEffect(() => {
onMessage((data) => {
console.log('Received:', data);
});
}, [onMessage]);
return <div>Status: {state.state}</div>;
}import {
WebRTCConnection,
FirebaseSignalingClient,
} from '@grndd-systems/zk-proof-rtc/core';
// Create signaling client
const signalingClient = new FirebaseSignalingClient({
firebaseConfig: {
apiKey: 'YOUR_API_KEY',
// ... other config
},
});
// Create connection
const connection = new WebRTCConnection(signalingClient, {
debug: true,
iceGatheringTimeout: 3000,
});
// Listen for messages
connection.on('message', ({ data }) => {
console.log('Received:', data);
});
// Listen for state changes
connection.on('state:change', ({ state }) => {
console.log('Connection state:', state);
});
// Create offer (desktop)
const peerId = await connection.createOffer('peer-123');
console.log('Share this ID:', peerId);
// Send data
connection.send({ type: 'test', payload: 'Hello!' });Implement the SignalingClient interface to use your own signaling server:
import { SignalingClient } from '@grndd-systems/zk-proof-rtc/core';
class MyCustomSignaling extends SignalingClient {
async initialize() {
// Connect to your signaling server
}
async createOffer(peerId, offer, metadata) {
// Upload offer to your server
}
async getOffer(peerId) {
// Retrieve offer from your server
}
// ... implement other methods
}
// Use with WebRTCConnection
const connection = new WebRTCConnection(new MyCustomSignaling());import { useWebRTCConnection } from '@grndd-systems/zk-proof-rtc/react';
import { MyCustomSignaling } from './my-signaling';
function MyComponent() {
const signalingClient = useMemo(() => new MyCustomSignaling(), []);
const { state, createOffer, send, onMessage } = useWebRTCConnection({
signalingClient,
config: { debug: true },
});
// ... use the hook
}Framework-agnostic WebRTC connection manager.
Constructor:
new WebRTCConnection(signalingClient: SignalingClient, config?: PartialWebRTCConfig)Methods:
createOffer(peerId: string, metadata?: SessionMetadata): Promise<string>- Initialize as offerercreateAnswer(peerId: string): Promise<void>- Initialize as answerersend(data: any): boolean- Send data through data channelclose(): Promise<void>- Close connection and cleanupgetState(): ConnectionState- Get current connection stategetPeerId(): string | null- Get peer IDisConnected(): boolean- Check if connected
Events:
state:change- Connection state changeddatachannel:state- Data channel state changedice:state- ICE connection state changedice:candidate- ICE candidate generatedmessage- Data receivederror- Error occurredpeer:connected- Peer connectedpeer:disconnected- Peer disconnected
React hook for WebRTC connection.
Options:
{
signalingClient: SignalingClient;
config?: PartialWebRTCConfig;
autoCleanup?: boolean; // default: true
}Returns:
{
state: UseWebRTCConnectionState;
createOffer: (peerId: string, metadata?: SessionMetadata) => Promise<string>;
createAnswer: (peerId: string) => Promise<void>;
send: (data: any) => boolean;
onMessage: (callback: (data: any) => void) => void;
close: () => Promise<void>;
}Convenience hook for Firebase signaling.
Options:
{
firebaseConfig?: FirebaseOptions;
firebaseApp?: FirebaseApp; // Use existing app
firebaseDatabase?: Database; // Use existing database
basePath?: string; // default: 'signals'
sessionTTL?: number; // default: 300000 (5 minutes)
webrtcConfig?: PartialWebRTCConfig;
autoCleanup?: boolean; // default: true
}Firebase Realtime Database signaling implementation.
Constructor:
new FirebaseSignalingClient(config: FirebaseSignalingConfig)Config:
{
firebaseConfig: FirebaseOptions;
firebaseApp?: FirebaseApp;
firebaseDatabase?: Database;
basePath?: string; // default: 'signals'
sessionTTL?: number; // default: 300000
debug?: boolean;
}interface WebRTCConfig {
iceServers: RTCIceServer[]; // default: Google STUN servers
channelName: string; // default: 'zkPassport'
channelOptions: RTCDataChannelInit; // default: { ordered: true }
iceGatheringTimeout: number; // default: 3000ms
sessionTTL: number; // default: 300000ms (5 minutes)
debug: boolean; // default: false
}@grndd-systems/zk-proof-rtc
├── core/ # Framework-agnostic core
│ ├── WebRTCConnection # Main connection manager
│ ├── EventEmitter # Type-safe event system
│ ├── SignalingClient # Abstract signaling interface
│ ├── FirebaseSignalingClient # Firebase implementation
│ └── types # TypeScript definitions
├── react/ # React integration
│ ├── useWebRTCConnection
│ └── useWebRTCWithFirebase
└── utils/ # Utilities
├── logger
└── generatePeerId
Desktop (Offerer):
import { useWebRTCWithFirebase, generatePeerId } from '@grndd-systems/zk-proof-rtc';
import QRCode from 'qrcode';
function DesktopApp() {
const [qrCode, setQrCode] = useState('');
const { state, createOffer, send, onMessage } = useWebRTCWithFirebase({
firebaseConfig: { /* ... */ },
webrtcConfig: { debug: true },
});
useEffect(() => {
onMessage((data) => {
console.log('Received from mobile:', data);
// Handle mobile response
});
}, [onMessage]);
const handleConnect = async () => {
const peerId = generatePeerId();
await createOffer(peerId);
// Generate QR code for mobile
const qrData = JSON.stringify({ peerId, timestamp: Date.now() });
const qrImage = await QRCode.toDataURL(qrData);
setQrCode(qrImage);
};
const handleSendRequest = () => {
send({
type: 'proof_request',
payload: {
requestId: '123',
proofType: 'age_check',
minAge: 21,
},
});
};
return (
<div>
{!state.isConnected ? (
<>
<button onClick={handleConnect}>Generate QR Code</button>
{qrCode && <img src={qrCode} alt="QR Code" />}
</>
) : (
<>
<p>✅ Connected!</p>
<button onClick={handleSendRequest}>Request Proof</button>
</>
)}
<p>Status: {state.state}</p>
</div>
);
}Mobile (Answerer):
import { useWebRTCWithFirebase } from '@grndd-systems/zk-proof-rtc';
function MobileApp() {
const [scannedData, setScannedData] = useState<{ peerId: string } | null>(null);
const { state, createAnswer, send, onMessage } = useWebRTCWithFirebase({
firebaseConfig: { /* ... */ },
});
useEffect(() => {
if (scannedData?.peerId) {
createAnswer(scannedData.peerId);
}
}, [scannedData, createAnswer]);
useEffect(() => {
onMessage((data) => {
console.log('Received from desktop:', data);
if (data.type === 'proof_request') {
// Generate and send proof
const proof = generateProof(data.payload);
send({
type: 'proof_response',
payload: proof,
});
}
});
}, [onMessage, send]);
return (
<div>
{!scannedData ? (
<QRScanner onScan={(data) => setScannedData(JSON.parse(data))} />
) : (
<p>Status: {state.state}</p>
)}
</div>
);
}MIT
Contributions are welcome! Please feel free to submit a Pull Request.
For issues and questions, please use the GitHub issue tracker.