Source of truth:
nostr://npub1s0vtechh66tx7vrwdud8zfyheu9zca7swwfrzd4qu2a4f93mxs6qvn9adx/nfx— issues and pull requests live there. This GitHub repository is a mirror; push to the Nostr remote, not here.
A Nostr-identified, peer-seeded video network: an open standard with a Rust reference implementation.
NFX lets a creator publish a signed video manifest on Nostr and lets peers seed and watch
the video, checking every file against signed hashes. The normative specification is in
spec/. The Rust reference implementation in crates/ covers iroh (native), a WebRTC
mesh (browsers) and an HTTP origin, which a public deployment puts behind TLS and a CDN.
The optional Hypercore profile (NFX-12) and payments (Cashu ecash, per delivered chunk)
are specified as drafts.
Status (2026-11-07): NFX 0.1.1 is the latest release (0.1.0 was the first): nfxd and
nfx-package for linux-x64 on the Releases page,
free video only (release notes; 0.1.0).
0.1.1's relay also searches a video's title. It is tested against the nutflix-pear desktop
app in both directions, behind NATs and over TLS relays. The public services on nfx:testnet:1 are an iroh
relay, https://iroh.nutflix.xyz/, and a scoped Nostr relay, wss://relay.nutflix.xyz (testnet). The free end-to-end slice (milestone M1: publish, seed, fetch,
play; no payments) is built and passed its exit run. That run and the Phase A exit run before it used two
hosts on a private network with plain HTTP and WebSocket endpoints; they did not cover
NAT traversal, TLS, a CDN or paid delivery. What each run left out:
M1,
Phase A.
The Hypercore profile (NFX-12) is not implemented. Payments are specified (NFX-07);
the open-mode contracts, the mock engine and the adversary suite sit in crates/nfx-pay
and crates/nfx-pay-wire, behind the lock, and are not deployed. NFX-01 to NFX-06 are
Frozen; the status of every document and its freeze milestone is in
spec/README.md. Log and resume point:
docs/nfx/status.md.
| Path | What it is |
|---|---|
spec/ |
NFX-01 to NFX-12, the JSON schemas (schemas/) and the regenerable test vectors (test-vectors/) |
crates/ |
The Cargo workspace (edition 2024), with the members below |
crates/nfx-proto |
Wire formats: manifests, beacons, hash lists, canonical JSON, vouchers, gossip envelopes. Pure (no I/O, clock or RNG); builds natively and for wasm32 |
crates/nfx-node |
The peer: content store, iroh seeding and fetching, gossip presence, Nostr manifests and beacons, pull-through origin, embeddable scoped relay, browser-mesh tracker and WebRTC bridge, and the Nostr-signaled full-ICE mesh endpoint |
crates/nfx-media |
NFX-05 packaging (ladder planning, CMAF output, content addressing); ships the nfx-package binary |
crates/nfx-wasm |
nfx-proto for browsers: verifies manifests, beacons, hash lists and files from JS |
crates/nfxd |
The headless node: key, publish, delete and run |
crates/desktop |
Tauri 2 desktop viewer on top of the nfxd daemon; its own Cargo workspace, sharing is opt-in |
crates/spikes |
Spike code and tools (s1-iroh, s2-tauri, nfx-verify-store), each its own workspace |
crates/ci |
check.sh, the lock scripts and the CI job definition |
crates/nfx-pay-wire, crates/nfx-pay |
The pay/1 wire and the open-mode payment contracts (NFX-07), with a mock engine and the adversary suite. Locked: see below |
web/player |
The browser test player: hls.js behind a loader that verifies every file with nfx-wasm, plus the WebRTC mesh |
web/spikes |
Browser spike code (s3-mesh, s4-cmaf) |
docs/ |
decisions/ (ADRs) and nfx/ (status, plans, exit runs, reviews, spike write-ups); index in docs/README.md |
Clone from Nostr (needs ngit's git-remote-nostr) or, instead, from the GitHub mirror:
git clone nostr://npub1s0vtechh66tx7vrwdud8zfyheu9zca7swwfrzd4qu2a4f93mxs6qvn9adx/nfx
# or: git clone https://github.com/SovereignTechnology/nfx
cd nfxPrerequisites: a Rust toolchain at least as new as rust-version in crates/Cargo.toml;
for the player, the wasm32-unknown-unknown target, wasm-pack and Node; for packaging,
ffmpeg and ffprobe on PATH (or NFX_FFMPEG and NFX_FFPROBE). The versions CI uses
are pinned in .github/workflows/ci.yml.
repo=$PWD # the root of the clone
cargo build --release --locked --manifest-path crates/Cargo.toml -p nfxd -p nfx-media
export PATH="$repo/crates/target/release:$PATH"
work=$(mktemp -d); echo "$work" # a scratch directory for the demo files
cd "$work"
cp /path/to/any-video.mp4 clip.mp4 # any local video that ffmpeg can readPackage the clip, create a key, and run a node that seeds it. --embed-relay and
--origin take ip:port; a bare :port is refused.
nfx-package clip.mp4 --video nfx:testnet:1:my-video --out pkg # pkg/store, pkg/nfx.json
pub=$(nfxd key new node.key | head -n 1) # prints the public key as hex, then as npub
A=38504:$pub:nfx:testnet:1:my-video # the manifest address
nfxd run --key node.key --store pkg/store --seed "$A" \
--embed-relay 127.0.0.1:7447 --origin 127.0.0.1:3473nfx-package makes one rendition by default: 720p, or the clip's own size when it is
smaller. A size is the picture's short side, so 720p is 1280x720, or 720x1280 for a
portrait clip. While the network is small, each extra rendition is another set of segments
to store and another browser swarm to split viewers across
(nutflix-pear#97 tracks a
fuller default). --renditions picks the sizes instead, from those the clip offers: the
tiers 2160p, 1440p, 1080p, 720p, 480p and 360p up to its size, and its own size
(--renditions 2160p for one 4K rendition, --renditions 1080p,720p,360p for the old
three-size ladder). Nothing is upscaled: a size the clip does not offer is refused.
In a second terminal, change to the scratch directory printed above and set repo (the
clone root) and PATH again. Publish the manifest to the embedded relay. It prints the
address, and the node then logs <address>: Seeding. Then serve the browser test player,
which listens on 0.0.0.0:3471:
nfxd publish --key node.key --relay ws://127.0.0.1:7447 --package pkg --title "My video"
cd "$repo/web/player" && npm ci --ignore-scripts && npm run serve
# open http://127.0.0.1:3471/?a=<address>&relay=ws://127.0.0.1:7447&origin=http://127.0.0.1:3473A node binds iroh to a random UDP port, and peers on the internet reach it through an iroh
relay (--iroh-relay) or by hole-punching. A host that can forward one UDP port to itself, a
home router's port forward or a DNAT in front of a VM, can be dialled directly instead:
nfxd run --key node.key --store pkg/store --seed "$A" --embed-relay 127.0.0.1:7447 \
--iroh-bind 0.0.0.0:7843 --iroh-external 203.0.113.7:7843Here 203.0.113.7:7843 stands for the public address and port forwarded to the host's UDP
port 7843. Tickets and beacons then name that address and leave out the host's private ones.
--embed-tracker and --bridge <ip:port> add the browser mesh (NFX-10): the bridge serves
the video to browsers over WebRTC, and its address is the one candidate they are told. Behind
a DNAT, bind --bridge to the host's own address and pass the public one as
--bridge-public (for example --bridge-public 203.0.113.7:7844); --bridge-max-peers
(default 64) caps the browsers it serves at once.
A node behind a NAT, a home node say, serves browsers with no tracker and no forwarded port
through the mesh endpoint (NFX-10 §6): --mesh-relay <url> (1 to 8 general Nostr relays;
a scoped relay refuses the signaling kind) turns it on, and --mesh-stun <ip:port> names a
public node's --stun so it learns its public mapping. Browsers that play a rendition it
holds meet it in that rendition's room, and both ends punch through their NATs with full
ICE:
nfxd run --key node.key --store pkg/store --seed "$A" --relay wss://relay.example \
--mesh-relay wss://general-relay.example --mesh-stun 203.0.113.7:3478Joining a room tells anyone present there the node's candidate addresses, its public mapping
included, as a tracker's swarm would. --mesh-bind fixes its UDP port and --mesh-max-peers
(default 16) caps its connections.
PYTHON=/path/to/venv/bin/python crates/ci/check.shcheck.sh stops at the first failing step. The step list, what each one needs and the
environment switches are in CONTRIBUTING.md. CI runs what
check.sh runs. The browser end-to-end suites (npm run e2e and npm run e2e:mesh in
web/player, ./e2e.sh in crates/desktop) are separate.
CI runs on GitHub Actions (.github/workflows/ci.yml) as two
jobs: lock, and check, which runs crates/ci/check.sh.
crates/ci/gitlab-ci.yml holds the same two jobs for GitLab
and is dormant.
The money code (crates/nfx-pay, crates/nfx-pay-wire and the paths listed in
crates/ci/check-locked.sh) is locked: every file is
pinned by hash in crates/ci/locked.sha256, and the lock fails any change that is not
re-pinned after review. The rule and its history are in
docs/nfx/m2-plan.md.
- spec/README.md: the suite, its reading order, status and conformance.
- docs/README.md: the index of the documentation tree.
- docs/nfx/status.md: current status, milestone log, what is next.
- ADR 0008: the master plan.
NFX grew beside a TypeScript demo, a Pear-runtime desktop app under the name nutflix,
which is a different design (ADR 0006).
Until 2026-10-11 this repository mirrored the demo's tree. The mirror was removed
(ADR 0008, amendment of that date):
the demo lives in its own repository, nutflix-demo, and the snapshot that was mirrored
here stays reachable under the tag demo-base:
git show demo-base:README.mdOlder records under docs/ that name packages/, scripts/ or docs/plan/ mean the
demo's tree at that tag.
See CONTRIBUTING.md, CODE_OF_CONDUCT.md and SECURITY.md (how to report a vulnerability).
Copyright (C) 2026 sovtech. Code is AGPL-3.0-or-later: see LICENSE. The
specification text in spec/ is public domain (CC0): see spec/README.md.