Skip to main content

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​

Supported Browsers​

BrowserMinimum VersionNotes
Chrome86+Full support including OPFS
Edge86+Full support including OPFS
Firefox78+No OPFS support yet
Safari14.3+Limited OPFS support
Opera72+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:

  1. 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'
}
]
}
}
  1. Check firewall settings
  2. 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:

  1. Check peer connection status
  2. Implement message acknowledgments
  3. 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:

  1. Network Tab: Monitor WebSocket connections
  2. Application Tab: Inspect IndexedDB and LocalStorage
  3. Console: View debug logs and errors
  4. WebRTC Internals: Chrome: chrome://webrtc-internals

Best Practices​

  1. Always use HTTPS in production
  2. Request notification permissions appropriately
  3. Handle connection failures gracefully
  4. Implement message queuing for offline support
  5. Monitor storage usage
  6. Validate all user inputs
  7. Use Web Workers for heavy processing
  8. Implement proper error handling
  9. Clean up resources on page unload
  10. Test across different browsers

Next Steps​