Skip to content

OpenDWeb · application-level networking

Multi-device apps need logical networks — game rooms, not system VPNs.

Devices drift across networks and addresses; what an app lacks is a room — a logical network it owns, joined by invitation, with stable identity and direct connections. OpenDWeb forms that logical network at the application level: Ed25519 EndpointId for identity, signed-fact rosters for membership, iroh/QUIC direct connections with a self-hostable relay as fallback — no system-level VPN dragging the whole machine in.

Ed25519 EndpointIdSigned-fact RosterQUIC direct + relay fallbackMIT OR Apache-2.0
GitHub ↗ Quick start
opendweb — server

$npx opendweb server

* opendweb server v0.4.2

> Local: http://localhost:8787

> Network: http://192.168.1.100:8787

gateway 8787 · rendezvous 8787 · relay 3340

The three-layer answer, then delivery

Layer 1 · Identity

Ed25519 EndpointId

Addresses change as devices move; members should not. Stable identity decoupled from network addresses — z-base-32 display form, the same node across address changes.

identity

Layer 2 · Roster

Signed facts, converged by union-merge

Membership verifiable without a central database: Genesis/Grant/Join/Revoke facts are signed, content-addressed (BLAKE3), and converge by union-merge.

roster

Layer 2 · Membership

Controlled, invite-based joins

A room is only a room if joining is by invitation. Issuer-online single redemption: challenge-response PoP plus invite_id CAS consumption — each token redeemed exactly once.

invites

Layer 3 · Session

iroh 1.1: QUIC direct + relay fallback

Members talk directly whenever the network allows. QUIC direct with NAT traversal; falls back to a self-hostable relay. Gating on both sides — gate before data; per-frame resource caps.

session

Payload

Opaque envelopes, bidirectional

The fabric does not dictate your data model. Send/receive opaque envelopes both ways; an Automerge adapter is planned as a separate change.

sync

Delivery · Self-hosting

One-liner server

The control plane is yours to host: npx opendweb server or docker ghcr.io/jixoai/opendweb — gateway 8787 + relay 3340.

server

Delivery · Plugins

Marketplace, vendor-neutral core

Vendor integrations are opt-in, not baked in. Any non-builtin first token dispatches adaptively; missing plugins fetch on first use. Cloudflare Tunnel ships as a plugin.

plugins

Delivery · SDK

Node SDK (napi-rs)

Embed the fabric instead of shelling out: @jixo/opendweb-client-sdk embeds fabrics in your own app (darwin-arm64 / win32-x64).

sdk

Quick start

Start the server, invite a peer, chat

The gateway (8787) serves /healthz, /services.json and rendezvous; the relay (3340) is a separate listener. Invites must be redeemed while the inviter is online — the inviter's process stays running during redemption.

quick-start.sh
# 1. Start the self-hosted server (gateway + relay) — top-level CLI
npx opendweb server
#   or: docker run -p 8787:8787 -p 3340:3340 ghcr.io/jixoai/opendweb
#   The banner lists every Network address. Any of them is the single
#   config entry for clients — the gateway discovers the relay URL
#   automatically via /services.json.

# 2. On each client machine: one-time config (persisted to ~/.opendweb/config.json)
npx @jixo/opendweb-example config set relay http://192.168.2.13:8787

# 3. Terminal A: initialize and keep a chat session running
npx @jixo/opendweb-example init --data ~/.dweb-a
npx @jixo/opendweb-example invite --data ~/.dweb-a --ttl 30m   # copy the token
npx @jixo/opendweb-example chat --data ~/.dweb-a

# 4. Terminal B (another directory/device): redeem the invite and chat
npx @jixo/opendweb-example join --data ~/.dweb-b <token>
npx @jixo/opendweb-example chat --data ~/.dweb-b
expected server banner
* opendweb server v0.4.2
> Local:   http://localhost:8787
> Network: http://192.168.1.100:8787

  NAME         PORT   STATE
  gateway      8787   entry point
  rendezvous   8787   merged into gateway
  relay        3340   enabled

Self-host without npx: docker ghcr.io/jixoai/opendweb. Behind a reverse proxy or tunnel, set DWEB_PUBLIC_GATEWAY_URL / DWEB_PUBLIC_RELAY_URL — see the README for the vendor-neutral recipe.

Packages

One CLI, one server binary, one SDK — all published

Current published line: opendweb 0.4.2 · client-sdk / server-binary / example 0.3.2 · ext-cf 1.0.3 · config 0.1.0. The server CLI is the marketplace host; plugins and the Node SDK extend the same fabric.

npm packageRole
opendwebServer CLI — `npx opendweb server` starts the self-hosted gateway + relay; plugin marketplace host
@jixo/opendweb-server-binaryServer binary wrapper used by the CLI; also exposes a programmatic `startServer()`
@jixo/opendweb-exampleReference two-process client CLI (`init` / `invite` / `join` / `chat`)
@jixo/opendweb-client-sdkNode SDK for embedding fabrics in your own app (napi-rs; darwin-arm64 / win32-x64)
@jixo/opendweb-config`definePlugin` helper for local plugin files (runtime-agnostic: deno / bun / node)
@jixo/opendweb-ext-cfCloudflare Tunnel plugin: ingress push via API, DNS routing, end-to-end verification, optional cloudflared co-spawn

Ecosystem

Vendor integrations are plugins; your app embeds the SDK

The CLI core stays vendor-neutral by construction — any non-builtin first token dispatches to a marketplace plugin. The Cloudflare Tunnel plugin covers no-public-IP deployments; the Node SDK embeds fabrics directly.

Plugin — Cloudflare Tunnel

opendweb — cf plugin
opendweb plugin add cf            # install (detected pm), lock name@version
opendweb cf setup --hostname opendweb.example.com
                                  # push ingress via CF API, route DNS,
                                  # write opendweb.config.toml, verify end-to-end
opendweb cf plan --hostname opendweb.example.com   # zero-side-effect preview

Node SDK — fabric in your app

sdk.cjs
const { Fabric } = require("@jixo/opendweb-client-sdk");

const relay = { mode: "custom", urls: ["http://192.168.2.13:3340"] };

// Machine A: create the fabric (this node becomes root) and sign an invite
const a = await Fabric.createRoot({ dataDir: "/path/a", relay });
const token = await a.invite(60 * 60_000, null); // dweb1.-prefixed token

// Machine B: redeem the token (issuer must be online) and exchange messages
const b = await Fabric.joinWithToken({ dataDir: "/path/b", relay }, token);
await b.connect(a.endpointId);
await a.send(b.endpointId, Buffer.from("ping"));
await a.revoke(b.endpointId); // root-only

Event stream: peer-connected / peer-disconnected, roster-updated, message, path-changed (direct / relay), relay-online / relay-offline. Full contract on GitHub.