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()
orbitdb
libp2p
helia
relay-discovery
Cause: 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()