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โ€‹