Freedom is a browser for the decentralized web, with Swarm, IPFS, Radicle, and ENS as first-class protocols. It ships with integrated Swarm, IPFS, and Radicle nodes, enabling direct peer-to-peer network access without relying on centralized HTTP gateways. Radicle is available on macOS and Linux; the Windows build ships without Radicle until official Windows binaries are published upstream.
-
Install Node.js 18+
-
Install dependencies:
npm install
-
Download the node binaries (first time only):
npm run ant:download npm run ipfs:download npm run radicle:download
-
Launch the app:
npm start
-
Swarm and IPFS nodes start automatically by default. To use
rad://, first enable Settings → Experimental → Enable Radicle integration (Beta). Enter a Swarm hash, IPFS CID, Radicle ID,bzz://URL,ipfs://URL,rad://URL, or.eth/.box/.wei/.gweidomain in the address bar.
Freedom Browser is an Electron application. Protocol logic lives in the main process; the renderer is a modular UI layer that talks to it over IPC (channels defined in src/shared/ipc-channels.js). The main process manages node lifecycles (ant-manager.js, ipfs-manager.js, radicle-manager.js), URL rewriting (request-rewriter.js), and persistent data (settings, bookmarks, history). A central service-registry.js tracks node endpoints, modes, and status, and broadcasts state to all windows — both node managers and the request rewriter read from it.
When a user enters a bzz://, ipfs://, ipns://, rad://, or ENS URL, the main process either dispatches to a custom protocol handler (bzz, ipfs, ipns) that proxies to the local node, or rewrites the URL to the active gateway URL via the registry (rad). rad:// handling is gated by the Radicle integration setting. bzz:// navigation is additionally gated by a cold-start probe (see next section). ipfs:// / ipns:// navigation goes straight to the native IPFS protocol handler, so no renderer warm-up probe is needed.
A fresh Swarm node pulls chunks on-demand through the DHT, and any individual chunk lookup can transiently fail with HTTP 404 even when the content is healthy and peers are connected. Across a page with 10–30 sub-resources (JS, CSS, fonts, images, video), a modest per-request failure rate compounds into visibly broken CSS, missing images, and videos that don't load. Retries almost always succeed — the problem is strictly first contact with cold content.
Freedom mitigates this in two layers:
-
Navigation probe (
src/main/swarm/swarm-probe.js) — before loading abzz://URL, the main process HEAD-polls/bzz/<hash>on the local node's gateway with exponential backoff (30 s per attempt, 5 min overall). The tab spinner runs during the probe, so the user never sees the node's raw 404 JSON; on timeout or node-unreachable we route toerror.htmlwith the original URL preserved and a Try Again button. Probes are cancellable. -
Custom
bzz:scheme (src/main/swarm/bzz-protocol.js) —bzzis registered as a privileged standard scheme, sobzz://<hash>/becomes the page origin in Chromium. Everybzz://request (top-level, sub-resources,fetch, mediaRange, CSS descendants, service workers) routes through a main-process handler that proxies to the local node's gateway, always setsSwarm-Chunk-Retrieval-Timeout+Swarm-Redundancy-Strategy: 3+Swarm-Redundancy-Fallback-Mode: true, retries transient5xxon idempotent methods with bounded exponential backoff (~50 s total) with a 30 s per-attempt deadline, and streams the response back.404responses are surfaced immediately so SPAs that feature-detect missing endpoints don't stall — the cold-start case that 404 retries used to absorb is now handled upstream by the navigation probe. Becausebzz://<hash>/is the origin, same-origin relative paths (/foo.js,url(/bg.png)) resolve naturally with no URL rewriting.
The handler also accepts Ethereum-named hosts: bzz://swarm.eth/..., bzz://site.wei/..., and bzz://apoorv.gwei/... resolve the contenthash via the in-process name resolver (cache hit after address-bar resolution) and proxy the same way. The page's URL/origin stays bzz://<name>/ rather than the resolved hash, so DevTools, window.location, storage, and subresource fetches like fetch('bzz://swarm.eth/data') see the name. Cross-transport mismatches (e.g. bzz://name.eth, bzz://name.wei, or bzz://name.gwei whose contenthash is IPFS) return 404 with an explanatory body — the typed scheme is treated as an assertion. The renderer's address-bar pipeline applies the same assertion before navigating, so both layers agree.
Origin model.
bzz://swarm.ethandbzz://<resolved-hash>are different origins from Chromium's perspective — cookies, localStorage, IndexedDB, and service workers are not shared between them. This mirrors HTTPS, wherehttps://example.comandhttps://192.0.2.1are also different origins even when they resolve to the same server. Pinning storage to the ENS name keeps state stable across contenthash updates.
Versions of Freedom before this change loaded bzz://<hash>/path by rewriting it to the gateway URL http://127.0.0.1:1633/bzz/<hash>/path and navigating there. Pages saw window.location.protocol === 'http:' and window.location.pathname === '/bzz/<hash>/path'.
With the custom scheme, pages now see:
window.location.protocol === 'bzz:'window.location.host === '<hash>'window.location.pathname === '/path'(the/bzz/<hash>/prefix is gone — it's encoded in the host)
Most sites work without changes — relative URLs (./assets/..., /foo.js, url(bar.png)) resolve naturally because bzz://<hash>/ is the origin. Sites break only when they sniff window.location to construct absolute gateway URLs.
Anti-pattern 1: protocol/pathname sniffing (e.g. tile servers, Leaflet maps):
// ✗ Old pattern — assumes the page is served from a gateway URL
const urlServer = window.location.protocol === 'http:' ? '' : 'http://localhost:1633';
const bzzMatch = window.location.pathname.match(/^\/bzz\/([\dA-Fa-f]{64})\//);
const bzzRoot = bzzMatch ? `/bzz/${bzzMatch[1]}/` : '';
const tileUrl = `${urlServer}${bzzRoot}{z}/{x}/{y}.png`;
// ✓ New pattern — relative URLs work from any origin (gateway or bzz://)
const tileUrl = './{z}/{x}/{y}.png';Anti-pattern 2: appending /bzz/<ref>/ to window.location.origin (e.g. SPAs fetching feeds, manifests, or other Swarm refs from JS):
// ✗ Old pattern — works under http://127.0.0.1:1633, builds garbage under bzz://
// becomes: bzz://<app-hash>/bzz/<feedRef>/ → 404
const feedUrl = `${window.location.origin}/bzz/${feedRef}/`;
// ✓ New pattern — point at the content ref directly with the bzz:// scheme
const feedUrl = `bzz://${feedRef}/`;
// ✓ Or, to support both gateway and native scheme modes:
const feedUrl =
window.location.protocol === 'bzz:'
? `bzz://${feedRef}/`
: `${window.location.origin}/bzz/${feedRef}/`;When you really need an absolute URL to your own bzz root:
const bzzRoot =
window.location.protocol === 'bzz:'
? `bzz://${window.location.host}/`
: `/bzz/${window.location.pathname.match(/^\/bzz\/([\dA-Fa-f]{64})\//)?.[1]}/`;Don't hardcode http://localhost:1633 — Freedom users may have their Swarm node on a different port, and external visitors via a public Swarm gateway certainly do.
ipfs and ipns are registered as privileged standard schemes (src/main/ipfs/ipfs-protocol.js), mirroring how bzz is wired up. Every ipfs://<cid|name>/... and ipns://<key|name>/... request — top-level navigation, sub-resources, fetch, media Range, CSS url(...), service workers — flows through a main-process handler that calls the embedded freedom-ipfs native request API and streams the response back. Because ipfs://<cid>/ (or ipfs://<name>/) is the page origin, window.location.protocol === 'ipfs:', same-origin relative paths Just Work, and storage (cookies, localStorage, IndexedDB, service workers) is keyed to the content reference.
The native request API accepts the same /ipfs/... and /ipns/... gateway-path shape, but Freedom no longer starts a loopback Kubo gateway or HTTP API for desktop IPFS. Status and progress shown in the UI come from native freedom-ipfs diagnostics.
ENS-named hosts work the same way as for bzz: ipfs://vitalik.eth/... resolves the contenthash via the in-process ENS resolver (cache hit after the address-bar resolution) and proxies the same way. The page's URL/origin stays ipfs://<name>/ rather than the resolved CID. Cross-transport mismatches (e.g. ipfs://name.eth whose contenthash is Swarm or IPNS) return 404 with an explanatory body — the typed scheme is treated as an assertion, matching the bzz handler's behaviour. Unlike bzz, the IPFS handler doesn't wrap its requests in a retry loop, and 4xx / 5xx responses pass through to the page so SPAs that feature-detect missing endpoints can render their own fallback.
Origin model. Same as
bzz:ipfs://vitalik.ethandipfs://<resolved-cid>are different origins from Chromium's perspective. Pinning storage to the ENS name keeps state stable across contenthash updates.
Because ipfs: and ipns: are standard schemes, Chromium's URL parser treats the host segment as a hostname and lowercases it. The base58btc encodings used by CIDv0 (Qm...), CIDv1 base58btc (z...), and IPNS peer-ID multihashes (12D3Koo..., 16Uiu2H..., Qm...) are case-sensitive, so a naïve ipfs://Qm.../path would arrive at the protocol handler as ipfs://qm.../path — different bytes, and the IPFS backend rejects it.
The address-bar / load pipeline in src/renderer/lib/url-utils.js (formatIpfsUrl → parseIpfsInput) canonicalises on the way in, before new URL sees the input:
- CIDv0
Qm...→ CIDv1 base32bafy... - CIDv1 base58btc
z...→ CIDv1 base32b... - base58btc IPNS peer ID → libp2p-key base36 (
k51...for Ed25519,k2k4...for sha2-256)
All target encodings are lowercase, so subsequent host normalisation by Chromium is a no-op. DNSLink names (docs.ipfs.tech) and ENS names (vitalik.eth) fall through unchanged — they're not base58btc and don't suffer the case issue. The encoders live in src/renderer/lib/cid-utils.js (kept inline because the renderer has no bundler).
If you click a <a href="ipfs://Qm.../"> link inside a page (rather than typing into the address bar), the webview preload intercepts the click in capture phase and reads the raw DOM attribute (getAttribute('href')) before Chromium resolves and lowercases the URL. It then sends that original mixed-case href to the host renderer, which routes it through formatIpfsUrl, so embedded link clicks canonicalise the same way address-bar input does. The interceptor covers same-tab clicks (click event) and modified-click / middle-click / target="_blank" / named-target dispositions (auxclick for real middle-click, since modern Chromium dispatches click only for the primary button) — without that, the new-window code path (Chromium → setWindowOpenHandler → tab:new-with-url) would receive the URL after Chromium had already lowercased the host, and case-sensitive bytes would be lost. The renderer's link:navigate IPC handler dispatches by disposition: same-tab calls go through loadTarget, new-tab calls go through openInNewTabWithTarget (the same helper the IPC tab:new-with-url path uses), so a target="docs" named target reuses an existing docs tab the way it does for plain HTTPS links.
Direct sub-resource fetches (<img src>, <script src>, <video src>, fetch(), CSS url(...)) cannot be intercepted in JS — by the time the request reaches the protocol handler, Chromium has already lowercased the host and the original CIDv0/base58btc bytes are gone. The handler detects this case and returns a clear 400 with an actionable message ("Publish the resource with its CIDv1 base32 (bafy...) form for sub-resource use.") rather than forwarding a guaranteed-bad reference to the native gateway. The same 400-with-explanation path covers lowercased CIDv1 base58btc (z...) hosts and lowercased base58btc IPNS keys. Site-author guidance: when emitting <img src> / <script src> / fetch() URLs in your own HTML, use CIDv1 base32 (ipfs://bafy.../...) and libp2p-key base36 (ipns://k51.../...) forms; both are case-insensitive and round-trip cleanly through Chromium's URL parser.
Gateway directory listings and gateway-authored pages can emit protocol-relative anchors like <a href="//localhost:8080/ipfs/<cid>">CID</a>. When the page origin is ipfs://<cid>/, Chromium resolves these against the page scheme and ends up with ipfs://localhost/ipfs/<cid> — which is no longer a valid IPFS reference (localhost isn't a CID). The same shape appears for absolute public-gateway links (ipfs://dweb.link/ipfs/<cid>/..., ipfs://ipfs.io/ipfs/<cid>/..., ipfs://cf-ipfs.com/ipfs/<cid>/..., etc.).
parseIpfsInput (renderer) and buildGatewayUrl (main) both recognise this gateway-form path and rewrite it to the canonical ipfs://<cid>/... (or ipns://<key>/... for cross-namespace cases like ipfs://localhost/ipns/..., ipfs://dweb.link/ipns/<dnslink-name>/...). The disambiguation rule has two parts:
- The outer host must be a recognised gateway. We use an explicit allowlist (
localhost,127.0.0.1,::1,*.localhost, plus the most common public gateways:dweb.link,ipfs.io,gateway.ipfs.io,cf-ipfs.com,cloudflare-ipfs.com,gateway.pinata.cloud,nftstorage.link,w3s.link,4everland.io,ipfs.fleek.co,dweb.eu.org). Earlier versions used a negative "host doesn't look like a content reference" heuristic, which over-fired for DNSLink hosts (e.g.ipns://docs.ipfs.tech/ipfs/coveragewould try to rewrite even thoughdocs.ipfs.techis the actual content host). Self-hosted gateways aren't matched — content authors with private gateways should publish canonicalipfs://<cid>/...URLs directly. The renderer-side check strips a trailing:<port>before the allowlist comparison soipfs://localhost:8080/ipfs/<cid>(Chromium's resolution of a gateway-authored protocol-relative anchor against the page'sipfs://origin — the port is preserved becauseipfs:has no default port) rewrites the same wayipfs://localhost/ipfs/<cid>does. - The embedded ref must be a CID or IPNS key (
/ipfs/<ref>and/ipns/<ref>), with the additional concession that/ipns/also accepts DNSLink-shaped names soipfs://dweb.link/ipns/docs.ipfs.tech/installrewrites toipns://docs.ipfs.tech/install. The legitimateipfs://<cid>/ipfs/<subfile>shape (a real subdirectory namedipfs) keeps loading from the host CID — the host CID isn't in the gateway list, so the path stays as-is.
For top-level navigation, the renderer rewrite means the address bar and page origin both end up canonical. For sub-resource fetches that go through the protocol handler directly, the bytes load but the URL Chromium associates with the resource stays in the gateway-host form — acceptable for sub-resources since they don't establish their own origin.
Versions of Freedom before this change loaded ipfs://<cid>/path by rewriting it to the path-gateway URL http://127.0.0.1:8080/ipfs/<cid>/path and navigating there — Chromium then followed Kubo's subdomain redirect to http://<cidv1>.ipfs.localhost:8080/path. Pages saw window.location.protocol === 'http:' and a host like <cidv1>.ipfs.localhost.
With the custom scheme, pages now see:
window.location.protocol === 'ipfs:'(or'ipns:')window.location.host === '<cid>'(or<name>,<key>)window.location.pathname === '/path'(the/ipfs/<cid>/prefix is gone — it's encoded in the host)
The Swarm migration guidance in the previous section applies verbatim — same anti-patterns (protocol/pathname sniffing, appending /ipfs/<cid>/ to window.location.origin), same fixes (relative URLs, or use the native scheme directly):
// ✗ Old pattern — assumes the page is served from the gateway
const apiBase = window.location.origin + '/ipfs/' + dataCid + '/';
// ✓ New pattern — use the ipfs:// scheme directly
const apiBase = `ipfs://${dataCid}/`;Don't hardcode http://localhost:8080 — Freedom no longer exposes a desktop IPFS loopback gateway, and external visitors via a public IPFS gateway may arrive through any gateway host.
Freedom runs Swarm, IPFS, and Radicle nodes, giving you access to three major decentralized networks from a single interface.
| Swarm | IPFS | Radicle | |
|---|---|---|---|
| Protocol | bzz:// |
ipfs://, ipns:// |
rad:// |
| Node Software | Ant (antd, bee-compatible) | freedom-ipfs native | radicle-node + radicle-httpd |
| Hash Format | 64 or 128-char hex (encrypted refs supported) | CIDv0 (Qm...) or CIDv1 (bafy...) |
Repository ID (z...) |
| Managed Gateway Port | 11633+ | internal native handler | 18780+ |
| Managed API Port | 11633+ | internal native handler | 18780+ |
| Managed P2P Port | 12633+ | internal native handler | 18776+ |
| Route Prefix | /bzz/{hash}/ |
/ipfs/{cid}/, /ipns/{name}/ |
/api/v1/repos/{rid}/ |
| Data Directory | <profile>/ant-data/ |
<profile>/ipfs-data/freedom-ipfs/ |
profile-scoped short Radicle home |
| Binary Directory | ant-bin/ |
native/freedom-ipfs-node/ |
radicle-bin/ |
Freedom manages nodes per browser profile:
- Independent Managed Nodes: By default, each profile starts its own Ant, native IPFS, and Radicle data directories. Ant and Radicle use profile-specific non-default ports; IPFS uses the embedded native handler without loopback API or gateway ports.
- Explicit External Nodes: Profiles can opt into external Swarm/Radicle endpoints in profile settings. External node identity and storage are shared outside that profile. IPFS always uses the embedded
freedom-ipfsnative node. - Port Conflict Handling: If a managed Ant or Radicle profile port is busy, Freedom picks a free profile port and persists the reassignment.
- Visual Feedback: The Nodes panel and profile settings show whether a node is managed, external/shared, or disabled.
This means Freedom works seamlessly whether you:
- Run it standalone (bundled Swarm and native IPFS nodes start automatically; Radicle is optional and behind an Experimental setting)
- Create multiple independent browser profiles with their own browser data, vault, and managed node state
- Already have system-wide Swarm/Radicle daemons running and explicitly configure a profile to use them
- Have port conflicts with other software (Freedom finds and records available profile ports)
On macOS, the packaged app explicitly allows multiple bundle instances so profile
launching can use open -n -a Freedom --args --profile=<id>.
- Toolbar Toggle: Click the network icon to access the Nodes panel with independent on/off switches.
- Live Statistics: View connected peers, visible network peers, and the Ant node version in real-time.
- DHT Client Mode: Runs in ultra-light mode for minimal bandwidth and resource usage.
- Automatic Configuration: First-run setup generates keys and config automatically.
- Independent Toggle: Start and stop IPFS separately from Swarm.
- Native Transport: Uses the embedded
freedom-ipfsnative addon instead of a loopback Kubo process. - Live Diagnostics: View native gateway stats and request progress while IPFS/IPNS pages load.
- Two-Process Architecture: Manages both
radicle-node(P2P network) andradicle-httpd(HTTP API) as a coordinated pair. - Automatic Identity: Creates a Radicle identity on first run (no manual setup required).
- Experimental Gate: Radicle is controlled via Settings → Experimental → Enable Radicle integration (Beta).
- Node Toggle: Once enabled, start and stop Radicle from the Nodes panel.
- Live Statistics: View connected peers, seeded repos, version, and Node ID.
- Repository Seeding: Seed Radicle repositories directly from the browser to help replicate them across the network.
- Stale Socket Cleanup: Automatically cleans up control sockets from unclean shutdowns.
- Port Conflict Resolution: Uses profile-specific managed ports and persists reassignment if one is unavailable.
- Windows: Radicle is not available on Windows yet (no upstream binaries). The Experimental settings section is hidden on Windows builds.
Enter any of the following in the address bar:
| Input Type | Example |
|---|---|
| Swarm Hash | a1b2c3... (64 or 128 hex characters) |
| Swarm URL | bzz://a1b2c3.../path/to/file.html |
| IPFS CID | QmHash... or bafybeic... |
| IPFS URL | ipfs://QmHash.../path |
| IPNS URL | ipns://k51... or ipns://domain.eth |
| Radicle ID | rad://z3gqc... |
| Ethereum Name | vitalik.eth, mysite.box, alice.wei, apoorv.gwei, mysite.eth/about |
| HTTP(S) URL | https://example.com |
| Domain | example.com (auto-prefixes https://) |
The address bar also provides autocomplete suggestions from browsing history as you type.
- Automatic Resolution:
.eth,.box,.wei, and.gweidomains resolve to their Swarm, IPFS, or IPNS content..ethand.boxuse ENS;.weiuses Wei Name Service (WNS);.gweiuses Gwei Name Service (GNS). - CCIP-Read Support:
.boxdomains resolve via offchain CCIP-Read (EIP-3668) through 3dns.xyz. - Protocol Detection: Automatically detects and routes to Swarm (
bzz://), IPFS (ipfs://), or IPNS (ipns://) content. - Transport-Aware Address Bar: After resolution, the address bar shows the resolved transport with the name as the host — e.g.
vitalik.ethresolves and displays asipfs://vitalik.eth, a Swarm-backedmysite.ethdisplays asbzz://mysite.eth, a WNS-backedalice.weidisplays asipfs://alice.wei, and a GNS-backedapoorv.gweidisplays asipfs://apoorv.gwei. The legacyens://form is still accepted as input (and stored bookmarks keep working) but is no longer the canonical display. - Typed Scheme Is an Assertion: Typing
bzz://name.eth,ipfs://name.eth,ipns://name.eth, or the equivalent.wei/.gweiforms only resolves if the contenthash matches the typed transport. Mismatches surface as a "resolves to X, not Y" message rather than silently switching transports — same rule thebzz://protocol handler enforces for subresource fetches. Bare names and the legacyens://form make no assertion and accept any supported transport. - Path Forwarding: Paths appended to names (e.g.,
mysite.eth/docs,alice.wei/docs,apoorv.gwei/docs) are preserved after resolution. - In-HTML Links: Ethereum name links inside web pages must carry a scheme —
ens://name.eth,bzz://name.eth,ipfs://name.eth,ipns://name.eth,bzz://name.wei,ipfs://name.wei,ipns://name.wei,bzz://name.gwei,ipfs://name.gwei, oripns://name.gwei. Bare hrefs like<a href="vitalik.eth">are relative URLs by HTML/URL-spec rules and resolve against the page's base before any of our handlers see them; bare names are only resolved in the address bar, where input is always absolute.
- Multiple Tabs: Open multiple pages simultaneously with
Cmd+T. - Tab Management: Close tabs with
Cmd+Wor middle-click. - Drag & Drop Reordering: Rearrange tabs by dragging.
- Per-Tab State: Each tab maintains its own navigation history, address bar state, and bzz/ipfs base.
- Link Handling: Links that open new windows are captured and opened in new tabs instead.
- Back/Forward: Standard browser history navigation per tab.
- Reload: Refresh the current page (ignores cache). On error pages, retries the original URL.
- Stop: Cancel page loading mid-request.
- Home: Return to the welcome page.
- Keyboard Shortcuts:
Cmd+N/Ctrl+N: New windowCmd+T/Ctrl+T: New tabCmd+W/Ctrl+W/Ctrl+F4: Close tabCmd+Shift+T/Ctrl+Shift+T: Reopen last closed tabCtrl+PgDn/Ctrl+Tab: Next tabCtrl+PgUp/Ctrl+Shift+Tab: Previous tabCtrl+Shift+PgDn: Move tab rightCtrl+Shift+PgUp: Move tab leftCmd+R/Ctrl+R: Reload (from cache)Cmd+Shift+R/Ctrl+Shift+R: Hard reload (bypass cache)Cmd+Shift+B/Ctrl+Shift+B: Toggle bookmark barF11: Toggle fullscreenF12/Cmd+Alt+I/Ctrl+Shift+I: Developer ToolsCmd+=/Ctrl+=: Zoom inCmd+-/Ctrl+-: Zoom outCmd+0/Ctrl+0: Reset zoomCmd+P/Ctrl+P: PrintEscape: Stop loading or restore address bar
- Address Bar Star: Click the star icon to bookmark or unbookmark the current page.
- Supported Protocols: Bookmark any
bzz://,ipfs://,ipns://,rad://,http://, orhttps://URL. - Named Bookmarks: Name and edit bookmarks via modal or right-click.
- Bookmarks Bar: Quick access below the toolbar, with an overflow menu when bookmarks don't fit. Always visible on the new tab page; toggle visibility on other pages with
Cmd+Shift+B/Ctrl+Shift+B(persisted across sessions).
- Automatic Recording: Pages are recorded as you browse.
- History Page: View and search your browsing history at
freedom://history.
Right-click on pages for context-sensitive actions:
- Page Context: Back, Forward, Reload, View Page Source, Inspect
- Link Context: Open Link in New Tab, Open Link in New Window, Copy Link Address
- Selection Context: Copy selected text
- Image Context: Open Image in New Tab, Save Image As, Copy Image, Copy Image Address
- View Page Source: Opens
view-source:URL in a new tab
- Automatic Path Rewriting: Absolute paths in decentralized content (e.g.,
/images/logo.png) are automatically rewritten to stay within the current hash/CID for IPFS (/ipfs/,/ipns/) and Radicle (/api/v1/repos/) content. - Per-Tab Tracking: Each tab tracks its own content base for correct path resolution.
- Swarm (
bzz://): Handled by a custom protocol handler rather than gateway rewriting — see Swarm Content Retrieval.
- Toggle via Menu: Open the hamburger menu (☰) and click "Debug Console".
- Console Logs: Captures JavaScript console output from loaded pages.
- Navigation Events: Shows page load, navigation, and error events.
- Timestamps: All messages include timestamps for debugging.
- Clear/Close: Clear the log or close the panel with dedicated buttons.
- CLI Logging: Debug messages also appear in the terminal.
Access built-in browser pages using the freedom:// protocol:
| Page | Description |
|---|---|
freedom://home |
Welcome/home page |
freedom://history |
Browsing history |
freedom://links |
Link behavior test page |
freedom://protocol-test |
Protocol and media test page |
rad://{rid} |
Radicle repository browser |
- Theme: Light, Dark, or System (follows OS preference).
- Node Auto-start: Toggle whether Swarm and IPFS nodes start automatically at launch (enabled by default).
- Experimental: Enable Radicle integration (Beta) and set
Start Radicle node when Freedom opens. - Auto-Updates: Toggle automatic update checks (enabled by default).
- Protocol Icons: Address bar shows Swarm (hexagon), IPFS (cube), Radicle (seedling), or HTTP (globe) icon based on current protocol.
- Hamburger Menu: Access browser features (New Tab, New Window, History, Zoom, Print, Developer Tools, Settings, About).
- Friendly Error Pages: Clear error messages with the original URL preserved.
- Feature-Gated Radicle Errors: Opening
rad://while integration is disabled shows:Radicle integration is disabled. Enable it in Settings > Experimental. - Retry on Reload: Pressing reload on an error page retries the original request.
- Graceful Degradation: Navigation errors don't crash the browser.
Freedom automatically manages node connections per profile. The default profile's managed endpoints start at:
- Swarm Ant:
http://127.0.0.1:11633 - IPFS: embedded native
freedom-ipfshandler; no desktop loopback gateway/API port is started - Radicle httpd:
http://127.0.0.1:18780
Named profiles use the next profile slot for Ant and Radicle (11634, 18781, and so on). The ecosystem default Swarm/Radicle ports (1633, 8780) are treated as external/system-node endpoints, not Freedom-managed defaults. IPFS is native-only and does not expose or reuse Kubo API/gateway ports.
If Freedom detects a compatible Swarm or Radicle daemon on an ecosystem default port for a protocol that would start at launch, it asks whether that profile should use the existing external node or keep an independent managed node.
For advanced users who need to connect a profile to a remote or system Bee/Radicle node, use Settings → Profiles → Node endpoints and switch the relevant protocol to external mode. Development-only renderer gateway overrides are still available via environment variables:
# Connect to a remote Swarm node
export ANT_API="http://remote-host:1633"
npm startInside Freedom, bzz://, ipfs://, ipns://, and rad:// URLs always resolve through the active profile's node settings and storage. OS-level protocol launches from other apps are a v1 limitation: they are not profile-aware and should not be used when a link must open in a specific profile. Open the target profile first and paste or navigate to the URL inside that window.
ENS, WNS, and GNS domains are resolved using Ethereum JSON-RPC. ENS uses the ENS Universal Resolver; WNS reads the Wei Name Service contract directly; GNS reads the Gwei Name Service contract directly. The browser tries multiple public RPC providers in sequence (see src/main/ens-resolver.js for the current list). You can prepend your own endpoint by setting the ETH_RPC environment variable.
Recommended: Helios Light Client
For trustless Ethereum data verification without running a full node, use Helios:
# Install Helios
curl https://raw.githubusercontent.com/a16z/helios/master/heliosup/install | bash
heliosup
# Run with an RPC provider
helios ethereum --execution-rpc https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY
# Configure Freedom to use local Helios
export ETH_RPC="http://127.0.0.1:8545"
npm startEdit src/renderer/pages/home.html to customize the welcome view shown on startup or when clicking Home.
| Script | Description |
|---|---|
npm start |
Launch the Electron app |
npm test |
Run unit tests (Jest) |
npm run test:e2e |
Run the harness E2E suite (stubbed nodes; fast, no network) |
npm run test:e2e:live |
Run the live E2E suite (real Ant + IPFS + ENS; manual only) |
npm run ant:download |
Download the Ant (antd) binary for your platform |
npm run ant:status |
Check the default profile's Freedom-managed Ant (11633) |
npm run system-ant:start / system-ant:status / system-ant:stop |
Run or inspect a repo-root system Ant on the ecosystem default port (1633) |
npm run ant:smoke-upload |
Exercise the buy → upload → download round-trip on a node |
npm run ipfs:download |
Download the pinned freedom-ipfs native addon |
npm run ipfs:native:smoke |
Smoke test the real native addon and live IPFS retrieval |
npm run ipfs:build |
Build the freedom-ipfs native addon from source |
npm run ipfs:reset |
Remove legacy repo-root IPFS data |
npm run build -- --mac --unsigned |
Build unsigned macOS app (for local testing) |
npm run dist -- --mac |
Build signed macOS distributable (DMG + ZIP) |
npm run dist:mac:prepare-notary |
Build signed macOS artifacts without notarization wait |
npm run dist:mac:submit-notary |
Submit macOS artifacts to Apple asynchronously |
npm run dist:mac:notary-status |
Check notarization status from saved receipts |
npm run dist:mac:notary-log -- <submission-id> |
Fetch notarization log JSON for a submission ID |
npm run dist:mac:staple-notary |
Staple and validate accepted notarized artifacts |
npm run dist:linux:arm64:docker |
Build Linux ARM64 via Docker (recommended) |
npm run dist:linux:x64:docker |
Build Linux x64 via Docker |
npm run dist -- --win |
Build Windows x64 distributable (NSIS + ZIP) |
The build and dist scripts accept --mac, --linux, or --win with optional --arm64, --x64, --unsigned, --no-notarize, and --verbose flags. See scripts/build.js for details.
ant:start is kept as a legacy alias for the explicit system-ant:start
script. It uses repo-root ant-data/, which Freedom no longer uses for
profile-managed dev data.
| Script | Description |
|---|---|
npm run radicle:download |
Download the Radicle binaries for your platform |
npm run radicle:init |
Initialize Radicle identity and configuration |
npm run radicle:status |
Check the default profile's Radicle httpd root endpoint |
npm run radicle:reset |
Delete all Radicle data and start fresh |
| Directory | Contents |
|---|---|
src/main/ |
Electron main process — node managers (Ant, IPFS, Radicle), ENS resolver, IPC, settings, history, bookmarks, auto-updater |
src/renderer/ |
UI — tabs, navigation, address bar, menus, context menus, bookmarks bar, debug console, settings modal |
src/renderer/pages/ |
Internal pages (home, history, error, links, protocol-test, rad-browser) |
src/shared/ |
Constants shared between main and renderer |
config/ |
Ant config template, default bookmarks, macOS entitlements |
scripts/ |
Build and setup helpers (binary downloads, Ant/IPFS/Radicle init) |
assets/ |
App icons |
Run all unit tests:
npm testThe suite covers most of src/main/ and src/renderer/lib/ — see src/**/*.test.js for the full list. Notable areas include:
- Networking & protocols:
bzz-protocol,swarm-probe,swarm-service,swarm-provider-ipc,request-rewriter,ens-resolver,ipfs-manager,radicle-manager,ant-manager,service-registry - Renderer navigation & UI:
navigation,navigation-utils,tabs,tabs-ui,bookmarks-ui,autocomplete,menus,page-context-menu,settings-ui,wallet/* - Identity, vault & wallet:
identity/derivation,identity/vault,identity/formats,wallet/dapp-permissions,wallet/transaction-service - Parsing & utilities:
url-utils,cid-utils,origin-utils,ethereum-uri,page-urls,brand - Storage & history:
bookmarks-store,settings-store,history,feed-store,publish-history
Two Playwright projects live under test-e2e/. The harness suite is run manually via npm run test:e2e; issue #69 regression (test-e2e/address-bar-clipboard.spec.js) runs on GitHub Actions for ubuntu-latest, windows-latest, and macos-latest (macOS runs the chrome context-menu specs only; Win/Linux menu wiring is skipped on Darwin). Configuration is in playwright.config.js.
| Suite | Command | Files | What it does |
|---|---|---|---|
harness |
npm run test:e2e |
test-e2e/*.spec.js |
Launches Electron with FREEDOM_TEST_MODE=1. The in-process harness in src/main/test-harness.js stubs Ant/IPFS startup, ENS resolution, the Swarm probe, and the bzz: / ipfs: / ipns: protocol handlers, so specs are fast (~15 s end-to-end), deterministic, and require no network or downloaded binaries. Covers address-bar normalisation, tabs, bookmarks, settings persistence, and the error-page flow. |
live |
npm run test:e2e:live |
test-e2e/live/*.spec.js |
Launches Electron without the harness — actual Ant + native IPFS startup, live ENS resolution, real bzz:// / ipfs:// protocol handlers. The live smoke waits for Swarm peers and for native IPFS to report running, then navigates to meinhard.eth (Swarm) and vitalik.eth (IPFS). Requires npm run ant:download and npm run ipfs:download first; missing binaries/addons skip before Electron launches. |
Both suites use a per-run temp userData directory (FREEDOM_TEST_USER_DATA) so they never touch your real settings, bookmarks, or history. Sequential runs only (workers: 1) — Electron + protocol-scheme registration and Ant port detection don't tolerate parallel app instances.
The main process uses electron-log with level-based transports:
| Environment | Console output | File output |
|---|---|---|
Development (npm start) |
info and above |
info and above |
| Production (packaged app) | warn and above |
info and above |
DEBUG=1 (any environment) |
verbose and above |
info and above |
Log files are written to the standard electron-log location (~/Library/Logs/Freedom/ on macOS).
To enable verbose logging in a packaged app:
DEBUG=1 /Applications/Freedom.app/Contents/MacOS/Freedom- Toggle the debug panel via Menu (☰) > Debug Console.
- Check the terminal for main process logs (visible at
infolevel and above in development):- Ant/IPFS/Radicle stdout and stderr
- IPC events
- Request rewrites
- ENS resolution
- Use Chrome DevTools in the webview (right-click > Inspect Element when available).
Create a distributable macOS app:
npm run dist -- --macFor local testing without code signing:
npm run build -- --mac --unsignedOutput goes to the dist/ folder as DMG and ZIP archives.
The build includes:
- Bundled Ant, freedom-ipfs native addon, and Radicle binaries
- Ant configuration template
- All renderer assets
Freedom uses better-sqlite3 for history and favicon caching, which is a native Node.js module. When cross-compiling for Linux from macOS, the native module must be compiled for the target platform.
Docker is required for Linux builds with working SQLite support:
# Build for Linux ARM64 (e.g., Raspberry Pi, ARM servers)
npm run dist:linux:arm64:docker
# Build for Linux x64
npm run dist:linux:x64:dockerThese commands run the build inside a Linux Docker container, ensuring native modules are compiled correctly. Docker Desktop must be running.
Note: The non-Docker Linux build commands (npm run dist -- --linux --arm64, npm run dist -- --linux --x64) only work when building on a native Linux machine of the matching architecture.
# Build Windows x64 distributable
npm run dist -- --win --x64
# Build Windows ARM64 distributable
npm run dist -- --win --arm64Output goes to the dist/ folder as NSIS installer and ZIP archive.
Note: Windows builds do not include Radicle binaries (no official upstream release yet). The Experimental settings section is hidden automatically on Windows.
For signed macOS builds with notarization, copy .env.example to .env and
fill in your Apple credentials:
APPLE_ID="your-apple-id@example.com"
APPLE_TEAM_ID="YOUR_TEAM_ID"
APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"The .env file is automatically loaded by the build scripts (via dotenv-cli),
so you don't need to manually export these variables. The signed build commands
(npm run dist -- --mac, npm run dist -- --mac --x64) will automatically use
these credentials for code signing and notarization.
Note: The .env file is git-ignored. Keep credentials out of the repo.
If Apple notarization might take a long time, you can split distribution into two phases so your terminal does not block:
-
Build signed artifacts without waiting for notarization:
npm run dist:mac:prepare-notary
-
Submit artifacts to Apple asynchronously (no
--wait):npm run dist:mac:submit-notary
-
Check notarization status later (safe after reboot/shutdown):
npm run dist:mac:notary-status
Inspect Apple processing details for a specific submission:
npm run dist:mac:notary-log -- <submission-id>
-
Once all submissions are
Accepted, staple and validate artifacts:npm run dist:mac:staple-notary
Submission receipts are stored in dist/notary-submissions/ so the process can
be resumed later. These scripts load the same .env Apple credentials used by
electron-builder (APPLE_ID, APPLE_TEAM_ID, APPLE_APP_SPECIFIC_PASSWORD),
so only one credential source needs to be maintained. If preferred, you can use
a keychain profile instead with NOTARY_PROFILE=your-profile.
Freedom includes non-intrusive auto-update functionality that silently checks for and downloads updates in the background.
Update Experience:
- Silent background downloads - no interruptions
- Updates install manually via notification/menu when ready
- Default-profile installs restart Freedom automatically; named/profile-dir installs close after install so users do not silently relaunch into the wrong profile
- Users stay in control - can disable in Settings
Server Setup:
After building distributable packages with npm run dist -- --mac, upload the following files to https://freedom.baby/downloads/:
latest-mac.yml # Auto-generated update metadata
Freedom-{version}-arm64-mac.zip
Freedom-{version}-arm64.dmg
Freedom-{version}-arm64-mac.zip.blockmap
Freedom-{version}-arm64.dmg.blockmap
Update Flow:
- App checks for updates 10 seconds after launch
- Checks again every 6 hours
- If new version available → downloads silently in background
- Small notification: "Update downloading..."
- When ready → notification: "Update ready. Click to install."
- Update installs via the notification or app menu; named/profile-dir profiles close after installation and should be reopened from the profile manager
Manual Update Check:
Users can manually check for updates via Menu (☰) → Check for Updates.
Disable Auto-Updates:
Users can disable automatic update checks in Settings → Updates.
Testing Updates Locally:
# Terminal 1: Start local update server
npm run serve:updates
# Terminal 2: Start app with updates enabled
npm run start:test-updater- Context Isolation: Uses
contextIsolation: trueandnodeIntegration: false. - Remote Module Disabled: The remote module is not available.
- Minimal API Surface: Only necessary IPC methods are exposed to the renderer. The
freedomAPI(history, bookmarks, etc.) is restricted to internalfreedom://pages — external websites cannot call it. - Local Nodes: Ant, IPFS, and Radicle run locally; no external services required for basic operation.
- Permission Handling: Pointer lock and fullscreen permissions are granted for better UX in Swarm/IPFS apps.
- Public RPC Fallback: ENS resolution uses public RPCs by default. For trustless verification, use a local Helios client.
- Freedom automatically detects managed-port conflicts and persists a free profile port
- If the node still fails, check terminal output for specific error messages
- Reset Ant data:
npm run ant:reset
- Freedom automatically detects managed-port conflicts and persists a free profile port
- Check for stale lock file: the app should auto-clean profile-local IPFS locks
- Reset IPFS data:
npm run ipfs:reset
- Ensure Settings → Experimental → Enable Radicle integration (Beta) is enabled
- Freedom automatically detects managed-port conflicts and persists a free profile port
- Ensure both
radicle-nodeandradicle-httpdbinaries exist inradicle-bin/ - If starting for the first time, Freedom creates a Radicle identity automatically
- Check terminal output for specific error messages
- Reset Radicle data:
npm run radicle:reset
- If you have a system-wide Swarm or Radicle daemon running, configure external mode in Settings → Profiles → Node endpoints
- External mode is per profile and per protocol
- The Nodes panel shows external/shared status when connected to an external node
- Freedom does not stop external nodes on quit
- Verify internet connectivity
- Check if public RPC providers are accessible
- For reliability, set a custom
ETH_RPCendpoint
- Ensure the respective node (Ant, IPFS, or Radicle) is running (check Nodes panel, for Radicle, first enable it in Settings → Experimental)
- Verify the Swarm reference (64 or 128 hex), CID, or Radicle ID is correct
- Check the debug console for error messages