Skip to content

Hub Setup

xNet works fully peer-to-peer — no server required. A Hub improves availability and adds services that are hard on mobile devices:

Without HubWith Hub
Sync only when both peers are onlineSync anytime — Hub bridges the gap
No backupEncrypted backup (zero-knowledge)
No full-text search across devicesServer-side FTS5 search
P2P onlyP2P when possible, Hub when not

The Hub never sees your plaintext data. It stores encrypted updates and relays them to your devices.

flowchart LR
  A[Device A] <-->|P2P| B[Device B]
  A <-->|Hub Relay| H[Hub]
  B <-->|Hub Relay| H

The fastest way to get a Hub running. No Docker, no SSH, no TLS setup.

Deploy on Railway

What happens:

  1. Railway clones the xNet repo
  2. Builds the Hub from its Dockerfile
  3. Creates a persistent volume for SQLite + blobs
  4. Gives you a URL: hub-xyz.up.railway.app

Cost: $0-2/month for a personal Hub (covered by Railway’s $5 Hobby credit).

Your Hub is live. Copy the URL and configure your app:

<XNetProvider
config={{
hubUrl: 'wss://hub-xyz.up.railway.app',
}}
/>
PlatformBest forTypical costNotes
RailwayOne-click, lowest friction$0-2/moIdeal default for personal Hubs
Fly.ioAuto-suspend, global edge$2-6/moSuspend/resume adds ~2s wake
VPSFull control$4-5/moYou manage TLS + upgrades

Best for auto-suspend cost savings or multi-region experiments.

Terminal window
# Install Fly CLI
curl -L https://fly.io/install.sh | sh
fly auth login
# Deploy
cd packages/hub
fly launch --no-deploy
fly volumes create xnet_hub_data --size 1 --region sjc
fly deploy

Cost: ~$2-6/month depending on usage. Machines can auto-suspend when idle.

For multi-region (future), add additional regions and volumes, then enable hub federation for query routing.

VariableDefaultDescription
PORT4444Listen port (Railway injects this)
HUB_DATA_DIR./xnet-hub-dataSQLite + blob storage directory
HUB_LOG_LEVELinfoLog verbosity: debug, info, warn, error
HUB_PUBLIC_URLPublic URL for discovery + federation
RAILWAY_VOLUME_MOUNT_PATHAuto-set by Railway when a volume is attached
FlagDescription
--portListen port
--dataData directory
--no-authDisable UCAN authentication
--storageStorage backend: sqlite, memory
--public-urlPublic hub URL for discovery
--max-connectionsMax concurrent WebSocket clients
--max-blob-sizeMax backup blob size
--awareness-ttlAwareness TTL in ms
--discovery-ttlPeer discovery TTL in ms
<XNetProvider
config={{
hubUrl: 'wss://your-hub.example.com',
}}
/>
const manager = new SyncManager({
hubUrl: 'wss://your-hub.example.com',
})
EndpointPurpose
GET /healthJSON status, uptime, platform metadata
GET /metricsPrometheus metrics
EndpointPurpose
PUT /backup/:docIdUpload encrypted backup
GET /backup/:docIdDownload encrypted backup
GET /backupList backups for your DID
PUT /files/:cidUpload content-addressed file
GET /files/:cidDownload file
POST /schemasPublish schema
GET /schemas/resolve/:iriResolve schema
POST /dids/registerRegister peer discovery
GET /dids/:didResolve peer

The Hub stores encrypted blobs. Your app encrypts before upload.

EndpointPurpose
PUT /backup/:docIdUpload encrypted backup
GET /backup/:docIdDownload encrypted backup
GET /backupList backups for your DID

Queries run locally first, with Hub results filling in the gaps:

const results = await queryClient.search('budget', {
federate: true,
limit: 20,
})

Moving between platforms is straightforward:

  1. Stop the old Hub (SIGTERM flushes SQLite)
  2. Copy the data directory (/data or your HUB_DATA_DIR)
  3. Start the new Hub with the same data dir
  4. Update your app’s hubUrl

Railway → VPS/Fly.io is just a volume export + upload. Fly.io → Railway is the same process in reverse.