Browser Usage Guide
This guide covers everything you need to know about using the Vaultys Peer SDK in web browsers.
Table of Contentsβ
- Browser Compatibility
- Installation
- Basic Setup
- Storage Options
- Security Considerations
- Performance Optimization
- Common Patterns
- Troubleshooting
Browser Compatibilityβ
Supported Browsersβ
| Browser | Minimum Version | Notes |
|---|---|---|
| Chrome | 86+ | Full support including OPFS |
| Edge | 86+ | Full support including OPFS |
| Firefox | 78+ | No OPFS support yet |
| Safari | 14.3+ | Limited OPFS support |
| Opera | 72+ | Full support including OPFS |
Required APIsβ
The SDK requires these browser APIs:
- WebRTC: For peer-to-peer connections
- IndexedDB: For data storage
- WebCrypto: For cryptographic operations
- File System Access API: Optional, for OPFS support
Installationβ
Using NPM/Yarnβ
npm install @vaultys/peer-sdk @vaultys/id
Using CDNβ
<script type="module">
import {
setupVaultysPeerSDK,
BrowserStorageProvider
} from 'https://cdn.jsdelivr.net/npm/@vaultys/peer-sdk@latest/dist/index.mjs';
import { VaultysId } from 'https://cdn.jsdelivr.net/npm/@vaultys/id@latest/dist/index.mjs';
</script>
Using Script Tagsβ
<!-- For modern browsers with ES modules -->
<script type="module" src="./node_modules/@vaultys/peer-sdk/dist/index.mjs"></script>
Basic Setupβ
Minimal Exampleβ
import { setupVaultysPeerSDK, BrowserStorageProvider } from '@vaultys/peer-sdk';
import { VaultysId } from '@vaultys/id';
async function initializePeerSDK() {
// Generate or load identity
const vaultysId = await VaultysId.generatePerson();
// Create peer service with browser storage
const peerService = setupVaultysPeerSDK({
vaultysId,
storageProvider: new BrowserStorageProvider(),
debug: true // Enable for development
});
// Set up event listeners
peerService.on('relay-connected', () => {
console.log('Connected to signaling server');
});
peerService.on('message-received', (message) => {
console.log('New message:', message);
});
// Initialize the service
await peerService.initialize();
return peerService;
}
Complete Browser Applicationβ
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>P2P Chat App</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.status {
padding: 10px;
background: #f0f0f0;
border-radius: 5px;
margin-bottom: 20px;
}
.status.connected {
background: #d4edda;
color: #155724;
}
.chat-container {
border: 1px solid #ddd;
border-radius: 5px;
padding: 20px;
height: 400px;
overflow-y: auto;
margin-bottom: 20px;
}
.message {
margin-bottom: 10px;
padding: 8px;
background: #f8f9fa;
border-radius: 3px;
}
.message.sent {
background: #e3f2fd;
text-align: right;
}
.input-group {
display: flex;
gap: 10px;
}
input, button, select {
padding: 10px;
border: 1px solid #ddd;
border-radius: 3px;
}
input {
flex: 1;
}
button {
background: #007bff;
color: white;
cursor: pointer;
border: none;
}
button:hover {
background: #0056b3;
}
</style>
</head>
<body>
<h1>P2P Chat Application</h1>
<div id="status" class="status">
Initializing...
</div>
<div>
<h3>Your DID</h3>
<input type="text" id="myDid" readonly style="width: 100%">
</div>
<div style="margin-top: 20px">
<h3>Add Contact</h3>
<div class="input-group">
<input type="text" id="contactDid" placeholder="Enter contact DID">
<input type="text" id="contactNickname" placeholder="Nickname (optional)">
<button onclick="addContact()">Add Contact</button>
</div>
</div>
<div style="margin-top: 20px">
<h3>Chat</h3>
<select id="contactSelect" style="width: 100%; margin-bottom: 10px">
<option value="">Select a contact</option>
</select>
<div class="chat-container" id="chatContainer"></div>
<div class="input-group">
<input type="text" id="messageInput" placeholder="Type a message" onkeypress="handleKeyPress(event)">
<button onclick="sendMessage()">Send</button>
</div>
</div>
<script type="module">
import {
setupVaultysPeerSDK,
BrowserStorageProvider
} from './node_modules/@vaultys/peer-sdk/dist/index.mjs';
import { VaultysId } from '@vaultys/id';
let peerService = null;
let currentContact = null;
// Initialize the SDK
async function initialize() {
try {
// Load or create identity
const identityKey = 'vaultys_identity';
let vaultysId;
const savedIdentity = localStorage.getItem(identityKey);
if (savedIdentity) {
vaultysId = VaultysId.fromSecret(savedIdentity);
console.log('Loaded existing identity');
} else {
vaultysId = await VaultysId.generatePerson();
localStorage.setItem(identityKey, vaultysId.getSecret());
console.log('Created new identity');
}
// Display DID
document.getElementById('myDid').value = vaultysId.did;
// Create peer service
peerService = setupVaultysPeerSDK({
vaultysId,
storageProvider: new BrowserStorageProvider(),
debug: true,
relay: {
host: 'peerjs.92k.de',
port: 443,
secure: true,
path: '/'
}
});
// Set up event listeners
setupEventListeners();
// Initialize service
await peerService.initialize();
// Load existing contacts
loadContacts();
} catch (error) {
console.error('Initialization failed:', error);
updateStatus('Failed to initialize: ' + error.message, false);
}
}
function setupEventListeners() {
peerService.on('relay-connected', () => {
updateStatus('Connected to server', true);
});
peerService.on('relay-disconnected', () => {
updateStatus('Disconnected from server', false);
});
peerService.on('message-received', (message) => {
displayMessage(message);
// Show notification
if (document.hidden && 'Notification' in window) {
if (Notification.permission === 'granted') {
new Notification('New Message', {
body: message.content,
icon: '/favicon.ico'
});
}
}
});
peerService.on('contact-online', (did) => {
console.log('Contact online:', did);
updateContactStatus(did, true);
});
peerService.on('contact-offline', (did) => {
console.log('Contact offline:', did);
updateContactStatus(did, false);
});
peerService.on('error', (error) => {
console.error('PeerService error:', error);
});
}
function updateStatus(message, connected) {
const statusEl = document.getElementById('status');
statusEl.textContent = message;
statusEl.className = connected ? 'status connected' : 'status';
}
window.addContact = async function() {
const did = document.getElementById('contactDid').value.trim();
const nickname = document.getElementById('contactNickname').value.trim();
if (!did) {
alert('Please enter a DID');
return;
}
try {
const contact = await peerService.addContact(did, {
nickname: nickname || did.slice(0, 20)
});
// Add to select
const select = document.getElementById('contactSelect');
const option = document.createElement('option');
option.value = contact.did;
option.textContent = contact.metadata.nickname + ' (offline)';
option.dataset.nickname = contact.metadata.nickname;
select.appendChild(option);
// Clear inputs
document.getElementById('contactDid').value = '';
document.getElementById('contactNickname').value = '';
console.log('Contact added:', contact);
} catch (error) {
alert('Failed to add contact: ' + error.message);
}
};
function loadContacts() {
const contacts = peerService.getContacts();
const select = document.getElementById('contactSelect');
contacts.forEach(contact => {
const option = document.createElement('option');
option.value = contact.did;
option.textContent = contact.metadata?.nickname + (contact.isConnected ? ' (online)' : ' (offline)');
option.dataset.nickname = contact.metadata?.nickname;
select.appendChild(option);
});
}
function updateContactStatus(did, isOnline) {
const select = document.getElementById('contactSelect');
const option = select.querySelector(`option[value="${did}"]`);
if (option) {
const nickname = option.dataset.nickname;
option.textContent = nickname + (isOnline ? ' (online)' : ' (offline)');
}
}
window.sendMessage = async function() {
const select = document.getElementById('contactSelect');
const input = document.getElementById('messageInput');
if (!select.value) {
alert('Please select a contact');
return;
}
if (!input.value.trim()) {
return;
}
currentContact = select.value;
try {
const message = await peerService.sendMessage(
currentContact,
input.value.trim()
);
displayMessage(message);
input.value = '';
} catch (error) {
alert('Failed to send message: ' + error.message);
}
};
function displayMessage(message) {
const container = document.getElementById('chatContainer');
const messageEl = document.createElement('div');
messageEl.className = message.from === peerService.vaultysId.did ? 'message sent' : 'message';
const time = new Date(message.timestamp).toLocaleTimeString();
const sender = message.from === peerService.vaultysId.did ? 'You' : getContactName(message.from);
messageEl.innerHTML = `
<strong>${sender}</strong> <small>${time}</small><br>
${escapeHtml(message.content)}
`;
container.appendChild(messageEl);
container.scrollTop = container.scrollHeight;
}
function getContactName(did) {
const contact = peerService.getContacts().find(c => c.did === did);
return contact?.metadata?.nickname || did.slice(0, 20);
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
window.handleKeyPress = function(event) {
if (event.key === 'Enter') {
sendMessage();
}
};
// Request notification permission
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
// Initialize on load
initialize();
</script>
</body>
</html>
Storage Optionsβ
Automatic Selectionβ
The BrowserStorageProvider automatically selects the best available storage:
const storage = new BrowserStorageProvider();
// Tries: OPFS β IndexedDB β LocalStorage
Force Specific Backendβ
// Force OPFS (modern browsers only)
const opfsStorage = new BrowserStorageProvider('OPFS');
// Force IndexedDB
const idbStorage = new BrowserStorageProvider('IndexedDB');
// Force LocalStorage (limited capacity)
const lsStorage = new BrowserStorageProvider('LocalStorage');
Storage Persistenceβ
Request persistent storage to prevent browser cleanup:
async function requestPersistence() {
if (navigator.storage && navigator.storage.persist) {
const isPersisted = await navigator.storage.persist();
if (isPersisted) {
console.log('Storage will not be cleared except by explicit user action');
} else {
console.log('Storage may be cleared under storage pressure');
}
}
}
Storage Quota Managementβ
Monitor and manage storage usage:
async function checkStorageQuota() {
if (navigator.storage && navigator.storage.estimate) {
const {usage, quota} = await navigator.storage.estimate();
const percentUsed = (usage / quota * 100).toFixed(2);
console.log(`Storage: ${usage} / ${quota} bytes (${percentUsed}% used)`);
if (percentUsed > 80) {
console.warn('Storage usage is high, consider cleanup');
await cleanupOldMessages();
}
}
}
async function cleanupOldMessages() {
const messages = await peerService.loadMessages(contactDid, 1000);
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
for (const message of messages) {
if (message.timestamp < thirtyDaysAgo) {
await peerService.deleteMessage(message.id, contactDid);
}
}
}
Security Considerationsβ
Content Security Policy (CSP)β
Configure CSP headers for your application:
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
connect-src 'self' wss://peerjs.92k.de https://peerjs.92k.de;
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
">
HTTPS Requirementβ
WebRTC requires HTTPS in production:
// Check if running on HTTPS
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
console.warn('WebRTC requires HTTPS for production use');
}
Input Validationβ
Always validate and sanitize user input:
function validateDID(did) {
const didPattern = /^did:vaultys:[a-zA-Z0-9]+$/;
return didPattern.test(did);
}
function sanitizeMessage(message) {
// Remove any potential XSS
const div = document.createElement('div');
div.textContent = message;
return div.innerHTML;
}
Secure Storageβ
Encrypt sensitive data before storing:
async function encryptData(data, password) {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(JSON.stringify(data));
const passwordBuffer = encoder.encode(password);
const passwordKey = await crypto.subtle.digest('SHA-256', passwordBuffer);
const key = await crypto.subtle.importKey(
'raw',
passwordKey,
{ name: 'AES-GCM' },
false,
['encrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
dataBuffer
);
return {
iv: Array.from(iv),
data: Array.from(new Uint8Array(encrypted))
};
}
Performance Optimizationβ
Lazy Loadingβ
Load the SDK only when needed:
async function loadSDKLazy() {
const { setupVaultysPeerSDK } = await import('@vaultys/peer-sdk');
return setupVaultysPeerSDK;
}
// Use when needed
document.getElementById('startChat').addEventListener('click', async () => {
const setupSDK = await loadSDKLazy();
const peerService = setupSDK({ /* config */ });
});
Message Paginationβ
Load messages in chunks:
class MessageLoader {
constructor(peerService, contactDid) {
this.peerService = peerService;
this.contactDid = contactDid;
this.offset = 0;
this.pageSize = 50;
this.hasMore = true;
}
async loadMore() {
if (!this.hasMore) return [];
const messages = await this.peerService.loadMessages(
this.contactDid,
this.pageSize,
this.offset
);
this.offset += messages.length;
this.hasMore = messages.length === this.pageSize;
return messages;
}
}
Web Workersβ
Offload heavy operations to Web Workers:
// worker.js
self.addEventListener('message', async (e) => {
const { type, data } = e.data;
switch (type) {
case 'process-file':
const processed = await processLargeFile(data);
self.postMessage({ type: 'file-processed', data: processed });
break;
}
});
// main.js
const worker = new Worker('worker.js');
worker.addEventListener('message', (e) => {
if (e.data.type === 'file-processed') {
handleProcessedFile(e.data.data);
}
});
function processFileInWorker(file) {
worker.postMessage({ type: 'process-file', data: file });
}
Common Patternsβ
Auto-reconnectionβ
Implement automatic reconnection on connection loss:
class ReconnectingPeerService {
constructor(config) {
this.config = config;
this.peerService = null;
this.reconnectTimeout = null;
this.reconnectDelay = 5000;
}
async connect() {
try {
this.peerService = setupVaultysPeerSDK(this.config);
this.peerService.on('relay-disconnected', () => {
this.scheduleReconnect();
});
await this.peerService.initialize();
this.reconnectDelay = 5000; // Reset delay on successful connection
} catch (error) {
console.error('Connection failed:', error);
this.scheduleReconnect();
}
}
scheduleReconnect() {
if (this.reconnectTimeout) return;
console.log(`Reconnecting in ${this.reconnectDelay}ms...`);
this.reconnectTimeout = setTimeout(() => {
this.reconnectTimeout = null;
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 60000);
this.connect();
}, this.reconnectDelay);
}
}
Message Queueβ
Queue messages when offline:
class OfflineMessageQueue {
constructor(peerService) {
this.peerService = peerService;
this.queue = [];
this.isOnline = false;
peerService.on('relay-connected', () => {
this.isOnline = true;
this.flushQueue();
});
peerService.on('relay-disconnected', () => {
this.isOnline = false;
});
}
async sendMessage(to, content) {
if (this.isOnline) {
try {
return await this.peerService.sendMessage(to, content);
} catch (error) {
this.queueMessage(to, content);
throw error;
}
} else {
this.queueMessage(to, content);
return { status: 'queued' };
}
}
queueMessage(to, content) {
this.queue.push({ to, content, timestamp: Date.now() });
this.saveQueue();
}
async flushQueue() {
const messages = [...this.queue];
this.queue = [];
for (const msg of messages) {
try {
await this.peerService.sendMessage(msg.to, msg.content);
} catch (error) {
this.queue.push(msg);
}
}
if (this.queue.length === 0) {
this.clearSavedQueue();
} else {
this.saveQueue();
}
}
saveQueue() {
localStorage.setItem('message_queue', JSON.stringify(this.queue));
}
clearSavedQueue() {
localStorage.removeItem('message_queue');
}
loadQueue() {
const saved = localStorage.getItem('message_queue');
if (saved) {
this.queue = JSON.parse(saved);
}
}
}
Troubleshootingβ
Common Issues and Solutionsβ
WebRTC Connection Failsβ
Problem: Peers can't establish connection Solutions:
- Add STUN/TURN servers:
relay: {
config: {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{
urls: 'turn:your-turn-server.com:3478',
username: 'username',
credential: 'password'
}
]
}
}
- Check firewall settings
- Verify both peers are online
Storage Quota Exceededβ
Problem: Storage operations fail Solution:
try {
await storage.write('file.txt', data);
} catch (error) {
if (error.name === 'QuotaExceededError') {
// Clear old data
await clearOldData();
// Retry
await storage.write('file.txt', data);
}
}
Messages Not Deliveredβ
Problem: Messages sent but not received Solutions:
- Check peer connection status
- Implement message acknowledgments
- Add retry logic
Debug Modeβ
Enable debug mode for detailed logging:
const peerService = setupVaultysPeerSDK({
debug: true, // Enable debug logs
relay: {
debug: 3 // PeerJS debug level (0-3)
}
});
// Custom debug logging
peerService.on('relay-connected', () => {
console.debug('[DEBUG] Connected to relay');
});
Browser DevToolsβ
Use browser DevTools for debugging:
- Network Tab: Monitor WebSocket connections
- Application Tab: Inspect IndexedDB and LocalStorage
- Console: View debug logs and errors
- WebRTC Internals: Chrome:
chrome://webrtc-internals
Best Practicesβ
- Always use HTTPS in production
- Request notification permissions appropriately
- Handle connection failures gracefully
- Implement message queuing for offline support
- Monitor storage usage
- Validate all user inputs
- Use Web Workers for heavy processing
- Implement proper error handling
- Clean up resources on page unload
- Test across different browsers
Next Stepsβ
- Learn about Node.js Usage
- Explore Message Handling
- Read about File Sharing
- Check out Examples