| name | local-dev |
| description | Set up and manage local Freenet development environments and interact with a running node. Use when the user wants to test contract changes locally, debug UI issues, run a local node, query connections/diagnostics, inspect the dashboard, use the WebSocket API, or iterate on a Freenet application without deploying to the live network. |
| license | LGPL-3.0 |
Freenet Local Development & Node Interaction
Guidance for running local Freenet nodes, publishing contracts, querying node state, and debugging dApps during development.
Prerequisites
which freenet fdev
rustup target add wasm32-unknown-unknown
Architecture
Ports & Services
| Service | Default Port | Flag | Purpose |
|---|
| Network (P2P) | 31337 | --network-port | Peer-to-peer connections |
| WebSocket API | 7509 | --ws-api-port | Client API (UI, CLI tools, fdev) |
HTTP Endpoints
| Endpoint | Purpose |
|---|
GET / | Home dashboard (auto-refreshes every 5s) |
GET /peer/{address} | Peer detail page |
GET /v1/contract/web/{key} | Contract web interface |
WS /v1/contract/command?encodingProtocol=native | WebSocket API v1 |
WS /v2/contract/command?encodingProtocol=native | WebSocket API v2 |
Dashboard
The home dashboard at http://localhost:7509/ shows:
- Connection status, peer count, own ring location
- Peer table: address, ring location, type (Peer/Gateway), bytes sent/received, connected duration
- External address (NAT traversal result), NAT statistics
- Contract counts (hosted, subscribed, managed)
- Operation stats (GET/PUT/UPDATE/SUBSCRIBE success/failure counts)
Scraping peer data:
curl -s http://localhost:7509/ | grep -o 'own-loc[^<]*<[^>]*>[^<]*'
curl -s http://localhost:7509/ | grep -o 'peer-row[^}]*'
Node Data Locations
| Platform | Default Data Path |
|---|
| macOS | ~/Library/Application Support/The-Freenet-Project-Inc.Freenet/ |
| Linux | ~/.local/share/freenet/ |
Contents: contracts/ (WASM), delegates/, secrets/, db/, config.toml
Log Directory Convention
Choose an appropriate log directory for your OS:
- macOS:
~/Library/Logs/freenet-test-node
- Linux:
~/.local/share/freenet-test-node/logs
The examples below use $LOG_DIR as a placeholder. Set it once:
LOG_DIR=~/Library/Logs/freenet-test-node
LOG_DIR=~/.local/share/freenet-test-node/logs
mkdir -p "$LOG_DIR"
Running Local Nodes
Single node (simplest)
Your existing node on port 7509 works. Publish test contracts to it directly.
Isolated test node (won't affect your running node)
IMPORTANT: Gateway nodes require --public-network-address. Always use
--log-dir to isolate logs from your main node.
freenet network \
--network-port 31338 \
--ws-api-port 7510 \
--ws-api-address 0.0.0.0 \
--is-gateway \
--skip-load-from-network \
--data-dir ~/freenet-test-node/data \
--public-network-address 127.0.0.1 \
--log-dir "$LOG_DIR" \
--log-level debug
Persistent data lives in --data-dir and logs in --log-dir, but the
gateway bootstrap list is NOT isolated by --data-dir — see
Isolation pitfalls below before assuming the node
is offline-only. Likewise, fdev defaults to port 7509 and will silently
target whichever node owns that port (often the system service, not your
test node).
WARNING: Do NOT use --id for local dev. It creates ephemeral temp directories
that get wiped on restart, destroying delegate secrets (signing keys, app data).
Use --data-dir for persistent isolation instead.
Isolation pitfalls
--data-dir does NOT isolate the gateway bootstrap list
freenet reads gateways.toml from the global config directory regardless
of --data-dir:
- macOS:
~/Library/Application Support/The-Freenet-Project-Inc.Freenet/gateways.toml
- Linux:
~/.config/freenet/gateways.toml
On a machine with an existing Freenet install, a "local" test node will
dial real public gateways (e.g. nova.locut.us, vega.locut.us) and
attempt NAT traversal to live peers — silently joining the public network.
To fully isolate, override HOME so the node sees an empty gateway list:
mkdir -p ~/iso-home/Library/Application\ Support/The-Freenet-Project-Inc.Freenet
printf 'gateways = []\n' > ~/iso-home/Library/Application\ Support/The-Freenet-Project-Inc.Freenet/gateways.toml
mkdir -p ~/iso-home/.config/freenet
printf 'gateways = []\n' > ~/iso-home/.config/freenet/gateways.toml
HOME=~/iso-home freenet network --is-gateway --skip-load-from-network ...
HOME=~/iso-home freenet network --gateway "127.0.0.1:31337,$GATEWAY_PUBKEY" ...
Note: an empty gateways.toml will fail with missing field 'gateways'.
The file must contain gateways = [].
Verification: grep the node log for the initial-join line and confirm
the gateway count matches what you passed:
grep "Starting initial join procedure" "$LOG_DIR"/freenet.*.log
Upstream tracking: freenet/freenet-core#3980.
fdev defaults to port 7509
fdev targets ws://127.0.0.1:7509 unless --port is passed. On a dev
machine running a system Freenet service (which owns 7509), fdev publish ...
without --port silently goes to that node, not your isolated test node.
fdev publish --code ... contract ...
fdev --port 7510 publish --code ... contract ...
Symptom of a misdirected publish: "Signature verification failed: signature error"
on a fresh publish to the test node, because the system node has stale
contract state from a previous run signed by a different key. If you see
this on a "fresh" test, check which node fdev actually hit.
--data-dir does NOT isolate config.toml either — use --config-dir per node
Two freenet processes on the same host that pass the same (or default)
config directory share config.toml AND secrets/transport-keypair.pem.
Symptoms: second node fails to bind its UDP port, or both nodes use
identical peer IDs and the network refuses the duplicate connection.
For a deterministic multi-node harness on one host (gateway + peer + …),
pass --config-dir explicitly to each node, NOT just --data-dir:
freenet network --config-dir /tmp/iso-net/gw/config --data-dir /tmp/iso-net/gw/data ...
freenet network --config-dir /tmp/iso-net/peer/config --data-dir /tmp/iso-net/peer/data ...
CI gotcha: on Linux runners that set XDG_CONFIG_HOME (e.g. ubicloud,
sometimes GitHub Actions images), dirs::config_dir() returns
$XDG_CONFIG_HOME regardless of HOME — so the HOME=~/iso-home …
trick from the previous section is bypassed. --config-dir is the only
flag that wins against XDG_CONFIG_HOME. Use it any time the harness
must run identically on dev laptops and CI.
A working reference harness lives at
scripts/run-isolated-nodes.sh in the freenet/mail repo — covers up /
down / wipe / status, full state wipe between test runs (avoids day-1
AFT cap carryover in repeated E2E runs), and FREENET_E2E_KEEP=1 to
leave nodes up for post-mortem.
Two-node local network
freenet network \
--network-port 31337 \
--ws-api-port 7509 \
--is-gateway \
--skip-load-from-network \
--data-dir ~/freenet-local-gw/data \
--public-network-address 127.0.0.1 \
--log-dir ~/freenet-local-gw/logs \
--log-level debug
GATEWAY_KEY=$(cat ~/.config/Freenet/secrets/local-gw/transport.pub 2>/dev/null || echo "CHECK_PUBKEY")
freenet network \
--network-port 31338 \
--ws-api-port 7510 \
--gateway "127.0.0.1:31337,${GATEWAY_KEY}" \
--skip-load-from-network \
--data-dir ~/freenet-local-peer/data \
--log-dir ~/freenet-local-peer/logs \
--log-level debug
Mobile testing (phone on same WiFi)
freenet network \
--ws-api-address 0.0.0.0 \
--ws-api-port 7510 \
--network-port 31338 \
--is-gateway \
--skip-load-from-network \
--data-dir ~/freenet-mobile-test/data \
--public-network-address 127.0.0.1 \
--log-dir ~/freenet-mobile-test/logs \
--log-level debug
Multi-instance deployment (10 peers + gateway)
cd /path/to/freenet-core
scripts/deploy-local-gateway.sh --all-instances
Publishing Contracts Locally
Using fdev
IMPORTANT: fdev argument order matters. --code and --parameters go
before the contract subcommand. --port goes before execute.
fdev --port 7510 execute put \
--code target/wasm32-unknown-unknown/release/my_contract.wasm \
--parameters params.bin \
contract \
--webapp-archive target/webapp/webapp.tar.xz \
--webapp-metadata target/webapp/webapp.metadata
fdev get-contract-id \
--code target/wasm32-unknown-unknown/release/my_contract.wasm \
--parameters params.bin
Targeting a specific node
Override the WebSocket port for fdev:
fdev --port 7510 execute put --code ... contract ...
Querying Node State
fdev query
fdev diagnostics
fdev diagnostics --contract <base58_contract_id>
WebSocket API
Connection
ws://127.0.0.1:7509/v1/contract/command?encodingProtocol=native
- Encoding:
native (bincode) or flatbuffers
- Auth: Send
ClientRequest::Authenticate { token } after connecting
- Tokens: Generated per-connection, base58-encoded 32 bytes. Invalidated on node restart (error prefix:
AUTH_TOKEN_INVALID).
Request Types
pub enum ClientRequest {
ContractOp(ContractRequest),
DelegateOp(DelegateRequest),
Authenticate { token },
NodeQueries(NodeQuery),
Disconnect { cause },
Close,
}
NodeQuery Variants
| Query | Response | Data |
|---|
ConnectedPeers | ConnectedPeers { peers } | Vec<(peer_id, socket_addr)> |
SubscriptionInfo | NetworkDebug(info) | Subscriptions + connected peers |
NodeDiagnostics { config } | NodeDiagnostics(response) | Configurable (see below) |
ProximityCacheInfo | ProximityCache(info) | Proximity cache state for update propagation |
NodeDiagnostics Config
NodeDiagnosticsConfig {
include_node_info: bool,
include_network_info: bool,
include_subscriptions: bool,
contract_keys: Vec<ContractKey>,
include_system_metrics: bool,
include_detailed_peer_info: bool,
include_subscriber_peer_ids: bool,
}
Wire-format change (stdlib v0.7.0): NodeDiagnosticsResponse.contract_states is now HashMap<String, ContractState> where the key is the Base58-encoded ContractKey::Display form (the instance field, not the full struct). Previously it was HashMap<ContractKey, ContractState> but that serialization broke JSON because the key was a struct. To map back to a typed ContractKey, decode the Base58 string and reconstruct. This is a bidirectional bincode break — match node and tooling versions.
for (key_str, state) in diag.contract_states.iter() {
}
Config File Reference
mode = "network"
network-address = "0.0.0.0"
network-port = 54761
ws-api-address = "0.0.0.0"
ws-api-port = 7509
min-number-of-connections = 25
max-number-of-connections = 100
transient-budget = 2048
transient-ttl-secs = 30
token-ttl-seconds = 86400
token-cleanup-interval-seconds = 300
log_level = "info"
is_gateway = false
Ring Distance
Each peer has a ring location in [0.0, 1.0). Distance between two locations:
distance = min(|a - b|, 1.0 - |a - b|)
Max distance is 0.5. Use the dashboard peer table to get locations and compute distances.
Debugging
Check node status
curl -s http://127.0.0.1:7510/
lsof -i :7510 -P | grep ESTABLISHED
ss -tnp | grep 7510
Check node logs
tail -f "$LOG_DIR"/freenet.$(date +%Y-%m-%d-%H).log
tail -f "$LOG_DIR"/freenet.*.log | grep -i "delegate\|contract\|websocket\|error\|sign"
Each node instance should use --log-dir pointing to a unique directory
so logs don't interleave.
Debugging node logs
Key patterns to search for:
grep -i "delegate\|sign\|update\|put\|subscribe" "$LOG_DIR"/freenet.*.log | tail -50
grep "01KK70QEAR" "$LOG_DIR"/freenet.*.log
grep -i "websocket\|connection\|disconnect\|client" "$LOG_DIR"/freenet.*.log | tail -20
Debugging with Playwright (automated browser testing)
Use the Playwright MCP tools to test the full UI flow without manual interaction:
1. browser_navigate → open the contract URL
2. browser_snapshot → see the DOM state
3. browser_click / browser_fill_form → interact with the UI
4. browser_console_messages → check for WASM panics or JS errors
Especially useful for reproducing mobile issues on desktop, where console
output is visible. If a flow works in Playwright but not on mobile, the
issue is likely WebSocket suspension or browser caching.
Mobile-specific debugging
Browser caching: Mobile browsers aggressively cache WASM bundles. After
republishing, use a cache-busting URL parameter:
http://{IP}:7510/v1/contract/web/{CONTRACT_ID}/?_v={timestamp}
Or clear browser cache / force close and reopen. Firefox mobile is
particularly aggressive about caching.
WebSocket suspension: Mobile browsers suspend WebSocket connections when:
- Screen locks
- Tab goes to background
- Browser switches to another app
- Heavy WASM computation starves the event loop
Your app should handle reconnection when the tab becomes visible again.
Consider implementing a visibilitychange listener that re-establishes
the WebSocket connection.
Common issues
| Symptom | Cause | Fix |
|---|
| "Gateway nodes must specify a public network address" | Missing --public-network-address | Add --public-network-address 127.0.0.1 |
| Signing key lost after node restart | Used --id (ephemeral temp dir) | Use --data-dir for persistent data |
| "Auth token not found" | Stale cached page | Hard refresh or clear browser cache |
| "delegate not found in store" | Legacy delegate migration | Expected on fresh node, non-blocking |
| "Connection reset by peer" | Browser killed WebSocket | Check if page is in background tab |
| "peer connection dropped" on put | Publishing to live node failed | Use isolated test node (--skip-load-from-network) |
| Contract not found | Not published to this node | Publish with fdev --port {PORT} (see Isolation pitfalls) |
| "Signature verification failed" on a fresh publish | fdev defaulted to port 7509 and hit the system node | Pass fdev --port {TEST_PORT} explicitly |
Test node joins public network despite --data-dir | gateways.toml is read from global config, not --data-dir | Override HOME to a sandbox dir with gateways = [] (see Isolation pitfalls) |
| Blank page (cached old WASM) | Mobile browser caches aggressively | Clear cache, force close browser, or use ?_v=timestamp |
sed -i fails on macOS | BSD sed requires backup extension | Use build tools directly instead of sed |
cargo make targets Linux | Cross-compilation for web-container-tool | Build natively: cargo build --release -p web-container-tool |
Other Infrastructure
Docker containers (freenet-core)
cd /path/to/freenet-core/docker/freenet-gateway
docker-compose up
cd /path/to/freenet-core/docker/freenet-node
docker-compose up
fdev simulation testing
fdev test --gateways 2 --nodes 10 --events 100 --seed 0xDEADBEEF single-process
fdev test --message-loss 0.1 --latency-min 50 --latency-max 200 single-process
Related Skills
- dapp-builder: Design and architect new Freenet dApps
- telemetry-monitor: Analyze network telemetry from the central collector
- release: Publish production releases