Developer docs

Documented against main @ bbedf98 (0.8.9-dev, 4 November 2026) · latest release v0.8.8

Radicle

window.radicle

Seed, sync, disclose identity, and write collaborative objects through the user's Radicle node. Repository reads are plain rad: URL fetches.

Source on GitHub
On this page

window.radicle lets a web page perform actions against the user's Radicle node: seed repositories, sync them, disclose the user's Radicle identity, and write collaborative objects (issues, issue comments, issue state changes and patch comments). Every action is scoped to the page's origin and gated by the user's consent. The provider reports specVersion: '0.2'.

Reads are rad: URL fetches#

The provider has no read methods for repository data. Freedom resolves the rad: URL scheme itself, so any page can fetch() repository metadata, trees, files, commits, issues and patches as JSON from the user's own node without a prompt. See rad: URLs for the routes.

Two limits apply: only public repositories are served (a private one is 403), and file, tree and commit routes take a full 40-character commit SHA, not a branch name.

The provider exists for everything that is not a public read:

Concern Why it needs consent
Seeding Commits the user's disk and bandwidth indefinitely
Syncing Uses bandwidth on demand
Identity disclosure The user's DID is a persistent cross-site identifier
COB writes Signed with the user's key; cannot be taken back once gossiped
Node introspection The seeded-repo list and node details are private information about the user

Provider object#

window.radicle is installed in the page's main world of every web page's top frame before any of the page's own scripts run, alongside window.swarm and window.ethereum. A page's Content Security Policy does not affect it. It is not installed in private windows, in sub-frames, or on Freedom's internal pages. Detect it with window.radicle?.isFreedomBrowser === true.

window.radicle.request({ method: string, params?: object }): Promise<any>

Every method also has a convenience wrapper without the radicle_ prefix, for example window.radicle.seed({ rid }) or window.radicle.getCapabilities().

Events#

window.radicle.on(event, handler)              // also addListener
window.radicle.removeListener(event, handler)
window.radicle.removeAllListeners(event?)
Event Payload When
connect { origin } The origin is granted access for the first time (a repeat radicle_requestAccess does not emit it)
disconnect { origin } Only after the page's own radicle_disconnect call
seedStatus a seed status object Replication progress for seed and sync actions this origin started

Freedom v0.8.8 has no settings screen that lists or revokes Radicle grants, so there is no user-side revocation that would emit disconnect. Treat a 4100 from a later call as the reliable signal that access is gone.

Timeouts#

The page shim rejects unanswered requests with { code: -32603, message: 'Request timed out' } after:

  • 300 s for methods that can open a consent prompt: radicle_requestAccess, radicle_seed, radicle_getIdentity and the four COB writes;
  • 60 s for everything else, including radicle_unseed, which does prompt. A user who takes longer than a minute to answer the unseed prompt gets a timeout on the page even though the decision still takes effect. This is a bug, tracked in #1001.

A timed-out promise does not cancel a grant or write that already reached the node, so retry COB writes carefully. A response that arrives after the tab navigated to another document is dropped, and the request rejects with 4900.

Errors#

Rejections are { code, message, data? }, aligned with window.swarm:

Code Meaning data.reason
4001 The user rejected the prompt —
4100 Not authorized not_connected (no connection grant), signing_not_granted (a signing-tier method without the signing grant), agent_custody (see Consent)
4200 Unknown method —
4900 Radicle unavailable profile-disabled (on every method, including radicle_getCapabilities), node-stopped (the node is not running or still starting)
-32602 Invalid params invalid_rid, invalid_id, invalid_title, invalid_body, invalid_labels, invalid_state, payload_too_large, not_seeded
-32603 Internal or node error repo_not_found, announce_failed, native_failed, seed_failed, unseed_failed, sync_failed, status_failed

The checks run in this order: unknown method (4200), Radicle disabled for the profile (4900 profile-disabled), then for every method except radicle_getCapabilities: no connection grant (4100 not_connected), signing tier without the signing grant (4100 signing_not_granted), and node not running (4900 node-stopped). Only radicle_getNodeStatus, radicle_requestAccess and radicle_disconnect work while the node is stopped.

Note the asymmetry while the node is starting: method calls fail with node-stopped, while radicle_getCapabilities reports reason: 'node-not-ready'.

Grants are keyed by the origin of the tab's address bar, derived by Freedom and never by anything the page controls. The key rules are the same as for window.swarm; see its origin key table.

Tier Methods Prompt
None radicle_getCapabilities never
Connection radicle_requestAccess once per origin
radicle_getNodeStatus, radicle_listSeededRepos, radicle_sync, radicle_getSeedStatus, radicle_disconnect never (need the connection grant)
radicle_seed, radicle_unseed every call, per repository
Signing radicle_getIdentity, radicle_createIssue, radicle_commentIssue, radicle_editIssueState, radicle_commentPatch the first call asks for the signing grant; later writes do not prompt
  • seed and unseed always prompt: one commits disk and bandwidth, the other drops a policy the user chose. The permission record has autoApprove flags, but v0.8.8 ships no UI to set them.
  • sync does not prompt, so it only accepts repositories that already have a seeding policy (-32602 not_seeded otherwise).
  • Writes sign with the user's node identity. There are no per-origin sub-identities: a forge needs the user to be the same author everywhere.
  • On a tab that Freedom Agent holds, the signing grant does not answer a write on its own: each write prompts with its content, and a write answered only by the grant is refused with 4100 agent_custody.
  • There is no per-origin rate limit on writes in v0.8.8.

Input rules#

  • Repository ids (rid) are accepted as rad:z…, rad://z… or a bare z…, where the z… part is 20 to 60 base58 characters. They are normalized to rad:z…. Anything else is invalid_rid.
  • COB ids (issueId, patchId, revisionId, replyTo) are 6 to 40 lowercase hex characters (invalid_id).
  • Text: titles, descriptions, comment bodies and labels must be non-empty after trimming whitespace.
  • Sizes: title at most 200 bytes, description and body at most 65,536 bytes (payload_too_large), each label at most 100 bytes, at most 10 labels (invalid_labels).

Methods at a glance#

Method Tier Returns
radicle_requestAccess connection { connected, origin, capabilities }
radicle_disconnect connection { connected: false }
radicle_getCapabilities none capability object
radicle_getNodeStatus connection node status
radicle_listSeededRepos connection seeded repositories
radicle_seed connection, prompts { rid, seeded: true, status }
radicle_unseed connection, prompts { rid, seeded: false }
radicle_getSeedStatus connection seed status
radicle_sync connection { rid, status }
radicle_getIdentity signing { did, nid, alias }
radicle_createIssue signing { id }
radicle_commentIssue signing { id }
radicle_editIssueState signing { id, state }
radicle_commentPatch signing { id }

Connection#

radicle_requestAccess#

Params none · Returns { connected: true, origin, capabilities } · Tier connection (prompts once)

Prompts the user to connect the origin. Repeat calls return the current state without a prompt. capabilities is the radicle_getCapabilities object. Works while the node is stopped. Emits connect on the first grant only.

radicle_disconnect#

Params none · Returns { connected: false } · Tier connection

The page gives up its own access: the connection grant, the signing grant and any auto-approvals. No prompt. Works while the node is stopped. Emits disconnect.

radicle_getCapabilities#

Params none · Returns capability object · Tier none

{
  specVersion: '0.2',
  canUseNode: boolean,   // connected AND node running
  reason: null | 'not-connected' | 'node-not-ready' | 'node-stopped',
  writes: ['issue', 'issueComment', 'issueState', 'patchComment']
}

When Radicle is disabled for the profile, this call fails with 4900 profile-disabled instead of returning an object.

Node#

radicle_getNodeStatus#

Params none · Returns node status · Tier connection

{
  running: boolean,   // status === 'running'
  status: string,     // the node's lifecycle status
  peers?: number,     // when running and the connection count can be read
  alias?: string,     // the public node alias, when running
  nid?: string        // only when the origin also has the signing grant
}

nid identifies the user, so it stays behind the signing grant. alias is gossiped network-wide anyway.

radicle_listSeededRepos#

Params none · Returns the repositories the node seeds · Tier connection

The repositories the user's node has a seeding policy for, including one whose first fetch has not finished yet. The list comes straight from the native Radicle library; expect at least each repository's rid, and treat metadata such as name and description as optional (it is missing until the first fetch lands). Unlike swarm_listFeeds, this needs a connection: the list is private information about the user.

radicle_seed#

Params { rid } · Returns { rid, seeded: true, status } · Tier connection, prompts per repository

Writes the seeding policy and starts a background fetch from the network. It resolves straight away; status has the radicle_getSeedStatus shape. Subscribe to seedStatus before you call it, and use radicle_getSeedStatus to restore the latest snapshot after a reload. This is how a page gets a repository the node does not have yet. A rejected prompt is 4001 and the node is not touched. Errors: invalid_rid, seed_failed.

radicle_unseed#

Params { rid } · Returns { rid, seeded: false } · Tier connection, prompts per repository

Removes the seeding policy and cancels any fetch in flight. Errors: invalid_rid, unseed_failed. Mind the 60 s timeout.

radicle_getSeedStatus#

Params { rid } · Returns seed status · Tier connection

A replication snapshot. seedStatus events carry the same shape.

{
  rid,
  state: 'fetched' | 'fetching' | 'failed' | 'cancelled' | 'idle',
  inStorage: boolean,           // the repo is served locally
  seedersKnown: number | null,  // network seeders found for the fetch
  attemptCount: number,
  recentAttempts: [{ nid, ok, error?, at }],  // last 5 per-seed results
  progress: { phase, candidates?, nid?, addr?, index?, total?, reason? } | null,
  lastError: string | null,
  startedAt: number | null,
  finishedAt: number | null
}

progress.phase is one of starting, resolving, connecting, fetching, peer-failed, done, failed or cancelled. There are no byte-level percentages. idle means nothing is known about the repository this session. A failed repository can still turn fetched later, because the node keeps retrying in the background. Errors: invalid_rid, status_failed.

radicle_sync#

Params { rid } · Returns { rid, status } · Tier connection

Restarts the background fetch for a repository that already has a seeding policy, for example after state: 'failed', without a second prompt. Follow seedStatus for progress. Errors: invalid_rid, not_seeded, sync_failed.

Identity and writes#

radicle_getIdentity#

Params none · Returns { did, nid, alias } · Tier signing

The user's Radicle identity. The first call asks for the signing grant, so this is the natural way to request it before the first write.

radicle_createIssue#

Params { rid, title, description, labels? } · Returns { id } · Tier signing

Opens an issue signed by the user and announces it to the network before resolving. Errors: invalid_rid, invalid_title, invalid_body, invalid_labels, payload_too_large, repo_not_found, announce_failed, native_failed.

radicle_commentIssue#

Params { rid, issueId, body, replyTo? } · Returns { id } · Tier signing

Comments on an issue, optionally as a reply to the comment replyTo. Errors as radicle_createIssue, plus invalid_id.

radicle_editIssueState#

Params { rid, issueId, state } · Returns { id, state } · Tier signing

state is 'open', 'closed' or 'solved' (invalid_state otherwise).

radicle_commentPatch#

Params { rid, patchId, body, revisionId? } · Returns { id } · Tier signing

Comments on a patch revision. Without revisionId, the comment goes on the patch's first revision (the patch id doubles as that revision's id).

Not in this version#

  • Creating patches or repositories (rad init): both need a working copy. Issues and patch comments cover the collaboration around existing patches.
  • Private repositories, through rad: URLs or the provider.
  • A settings screen to list or revoke Radicle grants, or to turn on auto-approval.
  • Per-origin write rate limits.
Design notes

One identity. Every origin with the signing grant acts as the user's single Radicle identity. A code forge is a reputation system: per-origin sub-identities, as window.swarm uses for feeds, would split the user into unlinkable authors and break Radicle's delegate model. The identity is only disclosed behind the signing grant.

Policy and replication are separate. A fetch can take seconds, fail per seed, or never complete, so radicle_seed resolves once the policy is written and reports replication through seedStatus.

In-process node. Every method calls Freedom's embedded Radicle library against the profile's own node and storage. No Radicle CLI, HTTP daemon or local port is involved. See Architecture.

Example#

if (!window.radicle?.isFreedomBrowser) throw new Error('Open this app in Freedom');

const rid = 'rad:z…';  // your repository id

window.radicle.on('seedStatus', (status) => {
  if (status.rid === rid) console.log(status.state, status.progress?.phase);
});

await window.radicle.requestAccess();        // prompts once
await window.radicle.seed({ rid });          // prompts for this repo

await window.radicle.getIdentity();          // asks for the signing grant
const { id } = await window.radicle.createIssue({
  rid,
  title: 'Typo in README',
  description: 'Second paragraph, "teh" should be "the".',
});