A local-first, peer-to-peer Todo application built with Svelte, Helia (IPFS), and OrbitDB for distributed data storage.
todo-p2p/
βββ π± src/ # Main application source
β βββ lib/ # Core P2P and OrbitDB logic
β β βββ orbitdb.js # OrbitDB database management
β β βββ p2p.js # Main P2P networking
β β βββ p2p-core.js # Core P2P utilities
β β βββ orbit-discovery.js # Peer discovery logic
β βββ routes/ # SvelteKit routes
β βββ utils/ # Utility functions
β βββ App.svelte # Main Svelte component
β βββ main.js # Application entry point
βββ π relay/ # Relay server for P2P networking
β βββ relay.js # Basic libp2p relay server
β βββ relay-enhanced.js # Enhanced relay with advanced features
βββ π§ͺ tests/ # Testing infrastructure
β βββ diagnostics/ # Diagnostic tools
β β βββ db-diagnostics.html # Interactive diagnostics UI
β β βββ debug-todos.js # Todo database diagnostic script
β β βββ inspect-storage.js # Command-line storage inspector
β βββ e2e/ # End-to-end tests (Playwright)
β β βββ todo.spec.js
β βββ node/ # Node.js integration tests
β β βββ bob.test.js # Multi-peer test scenarios
β β βββ manual.test.js # Manual testing utilities
β βββ standalone/ # Standalone network tests
β βββ test-discovery.html # WebRTC discovery testing
β βββ test-emitself-simple.js # emitSelf functionality test
β βββ test-pubsub-visibility.js # Pubsub self-message test
βββ βοΈ Configuration Files
β βββ package.json # Dependencies and scripts
β βββ vite.config.js # Vite bundler configuration
β βββ svelte.config.js # Svelte configuration
β βββ tailwind.config.js # TailwindCSS styling
β βββ playwright.config.js # E2E test configuration
β βββ eslint.config.js # Code linting rules
βββ π Documentation
βββ README.md # This file
Once you've created a project and installed dependencies with npm install (or pnpm install or yarn), start a development server:
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
The project includes several npm scripts for testing and diagnostics:
# Main test suites
npm test # Run all tests (e2e + node)
npm run test:e2e # Run Playwright end-to-end tests
npm run test:node # Run Node.js P2P integration tests
npm run test:manual # Run manual testing utilities
# Standalone network tests
npm run test:standalone:emitself # Test emitSelf functionality
npm run test:standalone:pubsub # Test pubsub self-message visibility
# Diagnostic tools
npm run diagnostics:inspect # Run command-line storage inspector
npm run diagnostics:todos # Todo database diagnostic script
# P2P infrastructure
npm run relay # Start basic libp2p relay server
npm run relay:enhanced # Start enhanced relay with advanced features
npm run relay:enhanced:verbose # Enhanced relay with detailed logging
Interactive tools (open in browser):
tests/diagnostics/db-diagnostics.html - Full diagnostic interfacetests/standalone/test-discovery.html - WebRTC discovery testingTo create a production version of your app:
npm run build
You can preview the production build with npm run preview.
To deploy your app, you may need to install an adapter for your target environment.
The Todo P2P app requires a libp2p relay server for peers to discover and connect to each other. Here's how to deploy it to a public server.
# Update system
sudo apt update && sudo apt upgrade -y
# Install Node.js 22 (if not already installed)
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
# Verify installation
node --version # Should be 22.x.x
npm --version
# Clone your repository
git clone https://github.com/your-username/todo-p2p.git
cd todo-p2p
# Install dependencies
npm install
# Create a production environment file
cp .env.example .env
# Edit .env if needed
# Allow port 4001 (relay server port)
sudo ufw allow 4001
# If using a cloud provider, also configure security groups
# to allow TCP traffic on port 4001
# Test run (foreground)
npm run relay
# Production run with PM2 (recommended)
npm install -g pm2
pm2 start relay/relay.js --name "todo-p2p-relay"
pm2 save
pm2 startup # Follow the instructions to auto-start on reboot
# Check if relay is running
netstat -tlnp | grep :4001
# Check PM2 status
pm2 status
# View logs
pm2 logs todo-p2p-relay
Create a Dockerfile in your project root:
FROM node:22-alpine
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application code
COPY relay/ ./relay/
COPY .env* ./
# Expose port
EXPOSE 4001
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S relay -u 1001
USER relay
# Start the relay server
CMD ["node", "relay/relay.js"]
Create docker-compose.yml:
version: '3.8'
services:
todo-p2p-relay:
build: .
ports:
- "4001:4001"
environment:
- NODE_ENV=production
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:4001/health', (res) => process.exit(res.statusCode === 200 ? 0 : 1))"]
interval: 30s
timeout: 10s
retries: 3
# Build and run
docker-compose up -d
# Or with plain Docker
docker build -t todo-p2p-relay .
docker run -d --name todo-p2p-relay -p 4001:4001 --restart unless-stopped todo-p2p-relay
# Check status
docker-compose ps
docker logs todo-p2p-relay
Once your relay server is deployed, update your app configuration:
In src/lib/p2p.js, change the relay address:
// Replace localhost with your server's IP or domain
const RELAY_BOOTSTRAP_ADDR = '/ip4/YOUR_SERVER_IP/tcp/4001/ws/p2p/12D3KooW...'
// Or use a domain name
const RELAY_BOOTSTRAP_ADDR = '/dns4/your-domain.com/tcp/4001/ws/p2p/12D3KooW...'
In tests/node/manual.test.js and tests/node/bob.test.js:
const RELAY_MULTIADDR = '/ip4/YOUR_SERVER_IP/tcp/4001/ws/p2p/12D3KooW...'
When the relay starts, it will log its Peer ID:
# Check relay logs to get the Peer ID
npm run relay
# or
pm2 logs todo-p2p-relay
# or
docker logs todo-p2p-relay
# Look for output like:
# π Relay server started
# π‘ Relay PeerId: 12D3KooWAJjbRkp8FPF5MKgMU53aUTxWkqvDrs4zc1VMbwRwfsbE
# π Listening on: /ip4/0.0.0.0/tcp/4001/ws
# Only allow necessary ports
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 4001
sudo ufw enable
For production, consider using HTTPS/WSS:
// In relay/relay.js, add TLS configuration
const server = createLibp2p({
// ... existing config
addresses: {
listen: [
'/ip4/0.0.0.0/tcp/4001/ws',
'/ip4/0.0.0.0/tcp/4002/wss' // Add WSS support
]
}
// Add TLS certificates configuration
})
Consider implementing rate limiting to prevent abuse:
# Install fail2ban
sudo apt install fail2ban
# Configure rate limiting rules
# (Implementation depends on your specific needs)
# Simple health check script
curl -f http://your-server:4001/health || echo "Relay server is down!"
# Add to crontab for automated monitoring
*/5 * * * * curl -f http://your-server:4001/health || /usr/bin/systemctl restart todo-p2p-relay
# With PM2
pm2 logs --lines 50
pm2 flush # Clear logs
# With Docker
docker logs --tail 50 todo-p2p-relay
docker logs --follow todo-p2p-relay # Live logs
# Direct deployment
cd todo-p2p
git pull origin main
npm install
pm2 restart todo-p2p-relay
# Docker deployment
git pull origin main
docker-compose down
docker-compose build
docker-compose up -d
Port 4001 not accessible:
# Check if process is running
netstat -tlnp | grep :4001
# Check firewall
sudo ufw status
# Check if bound to correct interface
ss -tlnp | grep :4001
Relay server crashes:
# Check system resources
free -h
df -h
# Check Node.js version
node --version # Should be 22+
# Check logs for errors
pm2 logs todo-p2p-relay --lines 100
Peers can't connect:
# Verify relay peer ID in app configuration
# Check network connectivity
telnet your-server-ip 4001
# Test WebSocket connection
# Use browser console: new WebSocket('ws://your-server-ip:4001')
tests/diagnostics/db-diagnostics.html in your browserOnce your app is running (npm run dev), open the browser console (F12) and use these functions:
// π Debug current todos and database state
await debugTodos()
// π§ͺ Run comprehensive OrbitDB operations test
await testOrbitDB()
// π₯ Run health check on all P2P components
await healthCheck()
// π§ Run health check with automatic recovery
await healthRecover()
// π Force reset all database components (soft reset)
await resetDB()
// π§Ή Nuclear option: Clear ALL OrbitDB data from browser
// This clears IndexedDB, localStorage, sessionStorage, and in-memory data
await window.app.clearAllOrbitDBData()
// π Get current database information
window.app.getCurrentDatabaseInfo()
// π Get current database address
window.app.getTodoDbAddress()
// π§ͺ Test pubsub self-message functionality
await testPubsub()
// π Debug pubsub configuration and state
debugPubsub()
// π‘ Test basic pubsub without self-messages
await testBasicPubsub()
// π Test OrbitDB topic subscription discovery
await testOrbitDBTopicSubscription()
// π₯ Get connected peers information
await window.app.getConnectedPeers()
// π Get your peer ID
window.app.getMyPeerId()
// π Check subscription change event listeners
debugSubscriptionListeners()
// π Get detailed connection information
window.app.getConnectionDetails()
// πΊοΈ Get peer OrbitDB address mappings
window.app.getPeerOrbitDbAddresses()
// π Open database for specific peer
await window.app.openTodoDatabaseForPeer('peer-id-here')
npm run dev
// Check if app is loaded
console.log('App available:', !!window.app)
// Run comprehensive diagnostics
await debugTodos()
await healthCheck()
orbitdblibp2pheliarelay-discoveryCause: CORS or network connectivity issues
// Clear browser cache
localStorage.clear()
// Check relay server connectivity
await window.app.getRelayDiscoveryStatus()
Cause: Database corruption or data structure issues
// Diagnose data structure
await debugTodos()
// Force database reset if needed
await window.app.forceResetDatabase()
Cause: Network timeout or P2P connection problems
// Check health and attempt recovery
await window.app.runDatabaseHealthCheckAndRecover()
// Nuclear option - full reset
await window.app.forceResetDatabase()
Open tests/diagnostics/db-diagnostics.html in your browser for a full GUI diagnostic tool with:
node tests/diagnostics/inspect-storage.js
node tests/diagnostics/inspect-storage.js help # For detailed help
Run individual P2P component tests for debugging:
# Test emitSelf functionality
node tests/standalone/test-emitself-simple.js
# Test pubsub self-message visibility
node tests/standalone/test-pubsub-visibility.js
# Open WebRTC discovery test interface
open tests/standalone/test-discovery.html
// Remove specific OrbitDB data
Object.keys(localStorage).forEach(key => {
if (key.includes('orbitdb') || key.includes('libp2p')) {
localStorage.removeItem(key)
console.log('Removed:', key)
}
})
Healthy todo should look like:
--- Todo 1 ---
ID: "1737307123456"
Text: "Buy groceries" β Should NOT be empty!
Text type: "string"
Text length: 13
Assignee: null
Completed: false
Created At: "2025-07-19T15:30:23.456Z"
Created By: "12D3K...AbCdE" β First 5 chars of PeerId
β
Todo appears healthy
If you need to completely start fresh and clear all P2P data:
// This will clear EVERYTHING: IndexedDB, localStorage, sessionStorage, and in-memory data
const report = await window.app.clearAllOrbitDBData()
console.log('Cleanup report:', report)
What this clears:
Expected output:
π§Ή Starting complete OrbitDB data cleanup...
1οΈβ£ Stopping all P2P components...
2οΈβ£ Clearing peer discovery data...
3οΈβ£ Clearing IndexedDB databases...
- Deleting database: orbitdb-keystore
- Deleting database: helia-datastore
- Deleting database: helia-blockstore
4οΈβ£ Clearing localStorage...
- Removing localStorage key: libp2p-relay-discovery
5οΈβ£ Clearing sessionStorage...
6οΈβ£ Clearing cached data structures...
π OrbitDB data cleanup completed!
// Returns cleanup report:
{
success: true,
actions: [
"Stopped P2P components",
"Cleared peer discovery data",
"Cleared IndexedDB databases",
"Cleared 3 localStorage keys",
"No relevant sessionStorage keys found",
"Cleared in-memory caches"
],
errors: []
}
// Reset P2P components but keep some cached data
await resetDB()
// Then refresh the page
location.reload()
// Clear absolutely everything
await window.app.clearAllOrbitDBData()
// Then refresh the page
location.reload()
// If functions don't work, clear manually
localStorage.clear()
sessionStorage.clear()
// Then refresh the page
location.reload()
When reporting issues, please include:
await debugTodos()await window.app.runDatabaseHealthCheck()