On this page
Abstract
This BSIP specifies the JSON-RPC 2.0 protocol, carried over a WebSocket, through which a client uses
a full Bitsocial node instead of joining the peer-to-peer network itself. Over one connection a
client reads comments, pages, and communities; subscribes to their updates; publishes comments,
votes, edits, and moderation through the challenge exchange; and creates, runs, edits, and exports
the communities the node hosts. Records cross the protocol unchanged, and the values the node
computes about them travel beside them in a separate runtimeFields object. The client signs every
publication itself, so the author’s key never reaches the node; the node runs the challenge exchange
on the client’s behalf and relays it. This BSIP defines the framing, authentication, the method
catalogue, subscriptions and their notifications, the state names clients rely on, errors,
compatibility rules, and what a client entrusts to its node.
Motivation
A full node fetches records from IPFS and gateways, joins gossipsub topics, resolves names against chains, and holds community keys and databases. Browsers and phones can do little of this: they cannot run an IPFS node, keep long-lived pubsub subscriptions, or stay online to publish a community. They connect instead to a node, the user’s own (for example a desktop daemon) or a hosted one, and ask it to do that work.
Web and mobile clients already depend on this protocol, and so do the nodes that serve them. Until now it was defined only by its implementation in pkc-js and by a README that had drifted from it. An independent node must answer the same methods with the same shapes, and an independent client must interpret the same notifications and states, or neither can replace the other. This BSIP is that shared definition. It also states plainly what a client gives up by delegating to a node, which matters more as hosted nodes serve users who do not run their own.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Terminology
- Node — the server side of this protocol: a full implementation that fetches and verifies records (BSIP-16), takes part in pubsub, resolves names, and may run communities.
- Client — the program connected to a node over this protocol.
- Local community — a community whose signing key and database the node holds; it appears in the node’s communities list. Every other community is remote to the node.
- Wire record — a signed or content-addressed record exactly as its BSIP defines it: a comment record (BSIP-2), a community record or page (BSIP-3), a comment update (BSIP-5), or a community list (BSIP-15). Messages of the challenge exchange (BSIP-4) cross this protocol decrypted and re-encoded (Records on the wire); the wire records inside them are verbatim.
- Runtime field — a value the node computes about a wire record: a name-verification outcome, a fetch state, a CID, a resolved IPNS chain.
- Subscription — a stream of notifications on one connection, created by a subscribe, start, or publish method and identified by a subscription id.
- Community identifier — the parameter object
{ "name"?: string, "publicKey"?: string }naming a community by its name, its identity key (BSIP-10), or both. At least one member is REQUIRED.
Transport and framing
A node exposes the protocol as a WebSocket endpoint (RFC 6455),
at ws:// or wss://. Every message is one text frame holding one UTF-8 JSON value, and follows
JSON-RPC 2.0 with the constraints below.
A request is:
{ "jsonrpc": "2.0", "id": 1, "method": "getComment", "params": [{ "cid": "Qm…" }] }
paramsis an array holding exactly one object, the method’s parameter object, or no element for a method that takes none. A node readsparams[0]and ignores any further elements.- A client MUST give every request an
id. A node executes a request withoutidbut sends no response, so its result, including any subscription id, is lost. - This protocol does not use JSON-RPC batches. The reference node accepts them; clients SHOULD NOT send them.
A response carries the request’s id and either result or error
(Errors). Every result is a JSON object, never a bare scalar, so that members can be
added later.
A notification is a message from the node with no id:
{
"jsonrpc": "2.0",
"method": "commentUpdateNotification",
"params": { "subscription": 7194037110274, "event": "updatingstatechange", "result": { "state": "fetching-ipfs" } }
}
params.subscriptionis the subscription id,params.eventthe event name, andparams.resultits payload, always a JSON object.- Clients MUST route a notification by
params.subscriptionandparams.event. Themethodname is informational and not consistent across subscription types (Subscriptions). - A node sends a subscription’s notifications in the order their events occurred. It MAY send them before the response that returns the subscription id; every subscription that has a current state sends it that way (Subscriptions). A client MUST therefore buffer notifications for subscription ids it does not know yet, and deliver them in arrival order once the id is known.
Connection and authentication
A node accepts two kinds of connection:
- Local: the connection comes from the node’s own host, meaning the socket’s remote address
equals its local address, and the upgrade request carries no
X-Forwarded-Forheader. A local connection needs no credential and uses the path/. - Remote: any other connection. Its request path MUST be exactly
/followed by the node’s auth key, for examplewss://node.example:9138/<authKey>, with no query string. A node without an auth key accepts local connections only.
A node rejects any other upgrade with HTTP status 403. A client that receives 403 MUST NOT reconnect
automatically: the credential is wrong, and retrying cannot fix it. Because the auth key travels in
the request path, a remote connection SHOULD use wss://.
Whoever holds a connection can call every method. The protocol has no accounts, roles, or per-method permissions; the auth key is a single bearer credential for the whole node.
Connection-scoped state. Every subscription belongs to the connection that created it. When a connection closes, the node ends all of its subscriptions: it stops in-flight publications, stops fetching updates on that connection’s behalf, and keeps no notifications for it. Two things outlive a connection:
- A local community started by
startCommunitykeeps running until a client callsstopCommunityordeleteCommunity, or the node shuts down. A node SHOULD record which local communities were started and not explicitly stopped, and start them again when it restarts. - Exports (Export) continue, and their files remain downloadable.
HTTP on the same port. The HTTP server that carries the WebSocket upgrade also serves
GET /exports/<exportId> (Export). That route takes no auth key: the export id is the
credential.
Records on the wire
Wire records travel verbatim. A wire record crosses the protocol as a JSON value, under a named
member of the payload (comment, commentUpdate, community, page), or as the whole result of
getComment. A node MUST NOT add, remove, or change any member of a wire record, at any depth,
including members it does not recognize, so that its signatures and CIDs stay verifiable on the
client (BSIP-12 gives the strictest case). The one
exception is the publication inside a challengerequest event of a start subscription, which the
node annotates (Subscriptions). Wire records carry no address; the client derives
runtime addresses as BSIP-10 defines.
Runtime fields travel beside them. Values the node computes about a record are sent in a
runtimeFields object next to the record, structured as the path inside the runtime object they
apply to: runtimeFields.author.nameResolved describes the author of the comment beside it, and
runtimeFields.comments[i] describes page.comments[i]. A client applies runtime fields by merging
them into its own runtime objects, never into the wire record, and a node MUST NOT write a runtime
field into a wire record. An absent runtime field means the node has no value for it.
| Runtime field | Where | Meaning |
|---|---|---|
nameResolved | community; author of a comment; comment.author at each level of a crosspost (BSIP-12) | The node’s outcome for the name: true verified, false refuted, absent unknown (BSIP-10). |
updateCid | community | CID of the community record the node holds. |
updatingState | community | The node’s updating state for the community when it built this payload. |
ipnsHops | remote community | The IPNS chain the node resolved, identity key first (BSIP-11). |
newPublicKey | remote community | The key a named community migrated to (BSIP-10); sent with resetInstance: true. |
posts.pages.<sort> | community | Page runtime fields for each preloaded page of posts. |
replies.pages.<sort> | comment update | Page runtime fields for each preloaded reply page. |
comments[i] | page | Runtime fields of the i-th page entry: author, crosspost, and replies.pages.<sort> for its nested reply pages. |
Names are resolved by the node. The node resolves every name with its own resolvers
(BSIP-10) and reports outcomes as nameResolved. A client does not
need resolvers of its own and, when it has none, MUST NOT treat a name it could not resolve as
refuted; it takes the node’s outcome. A node with no resolver for a name’s TLD fails any request that
needs that name with ERR_NO_RESOLVER_FOR_NAME, as a call error or as an error event of the
subscription.
Challenge-exchange messages. Notifications that carry a message of the challenge exchange
(BSIP-4) send it decrypted: the envelope members (type,
challengeRequestId, signature, encrypted, protocolVersion, userAgent, timestamp, and the
type-specific plaintext members such as acceptedChallengeTypes, challengeSuccess,
challengeErrors, reason) plus the members of its decrypted payload (comment, vote,
commentEdit, commentModeration, communityEdit, challengeAnswers, challengeCommentCids,
challenges, commentUpdate) at the top level. Byte strings are encoded as text:
challengeRequestIdas base58btc;signature.signature,signature.publicKey,encrypted.ciphertext,encrypted.iv, andencrypted.tagas base64 with the standard alphabet and no padding.
Other encodings. IPNS sequence numbers are decimal strings, because JSON numbers cannot hold a 64-bit unsigned integer exactly. Binary records a client hands to the node are base64 strings.
Methods
Each row gives the method, its parameter object (params[0]), and its result. Members marked ?
are OPTIONAL. Exact schemas are in the reference implementation.
| Method | Parameters | Result |
|---|---|---|
| Reading | ||
getComment | { cid, communityPublicKey?, communityName? } | The comment record |
getCommunityPage | { cid, type, pageMaxSize, communityPublicKey?, communityName? } | { page, runtimeFields? } |
getCommentPage | { cid, commentCid, pageMaxSize, communityPublicKey?, communityName? } | { page, runtimeFields? } |
fetchCid | { cid } | { content } |
resolveAuthorName | { name, cache?: { maxAge? } } | { resolvedAuthorName } |
fetchCommunityList | { cid } | { communityListRawString } |
| Subscribing | ||
communityUpdateSubscribe | community identifier | { subscriptionId } |
commentUpdateSubscribe | { cid, communityPublicKey?, communityName? } | { subscriptionId } |
communitiesSubscribe | none | { subscriptionId } |
settingsSubscribe | none | { subscriptionId } |
unsubscribe | { subscriptionId } | { success: true } |
| Publishing | ||
publishComment | { comment, challengeAnswers?, challengeCommentCids? } | { subscriptionId } |
publishVote | { vote, challengeAnswers?, challengeCommentCids? } | { subscriptionId } |
publishCommentEdit | { commentEdit, challengeAnswers?, challengeCommentCids? } | { subscriptionId } |
publishCommentModeration | { commentModeration, challengeAnswers?, challengeCommentCids? } | { subscriptionId } |
publishCommunityEdit | { communityEdit, challengeAnswers?, challengeCommentCids? } | { subscriptionId } |
publishChallengeAnswers | { subscriptionId, challengeAnswers } | { success: true } |
publishCommunityList | { communityListRawString } | { cid } |
| Running local communities | ||
createCommunity | community creation options | { localCommunity } |
startCommunity | community identifier | { subscriptionId } |
startedCommunitySubscribe | community identifier | { subscriptionId } |
stopCommunity | community identifier | { success: true } |
editCommunity | { name?, publicKey?, editOptions } | A local community payload |
deleteCommunity | community identifier | { success: true } |
prepareAnchorPublish | community identifier | { nextSequence, currentAnchorRecordSequence, hasPersistedAnchorRecord } |
publishAnchorRecord | { name?, publicKey?, recordBase64 } | { sequence, value, anchorPublicKey } |
| Export | ||
exportCommunity | { name?, publicKey?, includePrivateKey? } | { exportId } |
exportsSubscribe | community identifier | { subscriptionId } |
cancelExport | { exportId } | { success: true } |
exportCommunityModLogs | { name?, publicKey?, startTimestamp?, endTimestamp?, commentCid?, limit?, order? } | { moderations } |
| Node settings | ||
setSettings | { pkcOptions } | { success: true } |
There is no one-shot method for a community record. A client that wants a community once subscribes
with communityUpdateSubscribe, takes the first update, and unsubscribes.
Reading
The node fetches and verifies every record it returns (BSIP-16).
getCommentreturns the comment record forcid(BSIP-2), as the whole result.communityPublicKeyandcommunityNameare optional hints.getCommunityPagereturns one page of a community’s posts (type: "posts") or moderation queue (type: "modqueue") (BSIP-3). At least one ofcommunityPublicKeyandcommunityNameis REQUIRED.pageMaxSizeis the largest size in bytes the client accepts for this page, following the limits of BSIP-3; the node refuses a larger page.getCommentPagedoes the same for a page of the replies of the commentcommentCid(BSIP-5).fetchCidreturns the content stored undercidas a string. A client can check the bytes against the CID itself.resolveAuthorNameresolves an author name with the node’s resolvers (BSIP-10).resolvedAuthorNameis the key address the name designates, ornullwhen it designates none.cache.maxAgeis the oldest cached answer, in seconds, the node may return;0forces a fresh lookup.fetchCommunityListreturns a community list as the exact string stored undercid, unparsed; BSIP-15 specifies the checks on both sides.
Subscribing to communities and comments
communityUpdateSubscribe loads the community and keeps it updated, with the node’s resolvers and
its delegation checks (BSIP-11). If the node runs the community, the subscription
follows it instead of fetching it. Its update payload has one of two shapes:
- For a remote community, the remote community payload.
- For a local community, a local community payload.
commentUpdateSubscribe loads the comment record for cid and then keeps its comment update
current (BSIP-5). It sends one comment event with the
record, then an update event with the node’s latest comment update whenever the node’s copy
changes. An update can repeat one already sent; a client keeps the comment update with the highest
updatedAt. The reference client also sends parentCid, postCid, and, when it already holds them,
raw: { comment, commentUpdate }, so the node can skip a fetch; a node MAY ignore these members.
Publishing
The five publish methods take the decrypted payload of a CHALLENGEREQUEST
(BSIP-4): exactly one signed publication under its key — comment
(BSIP-2), vote (BSIP-6), commentEdit (BSIP-7),
commentModeration (BSIP-8), or communityEdit (BSIP-9) — and the
OPTIONAL challengeAnswers and challengeCommentCids.
- The publication MUST be complete and signed by the client,
signatureincluded. The node MUST NOT accept a private key in place of a signature, and MUST NOT re-sign or alter the publication. - Before signing, a client needs the target community’s identity key and name, which it signs into
the publication (BSIP-10). A client without its own network access gets
them from the node with
communityUpdateSubscribe. - The node loads the community, verifies the publication’s signature, and runs the challenge
exchange as the publishing side of BSIP-4: it generates the fresh exchange keypair, encrypts the
payload to the community, signs the envelopes, verifies the community’s replies, and decrypts them.
A node MAY retry over another pubsub provider with a new exchange; each attempt sends its own
challengerequestevent. - The node relays the exchange to the client as notifications:
challengerequest,challenge,challengeanswer, andchallengeverification, each carrying the decrypted message (Records on the wire), pluspublishingstatechange. - The client answers a
challengewithpublishChallengeAnswers, naming the publication by its subscription id.challengeAnswersis a non-empty array of strings matching the challenges by position. The node encrypts and signs theCHALLENGEANSWER. - The exchange ends with
challengeverification, or withpublishingstatechangetofailed. The client SHOULD thenunsubscribe. - If the publication fails before the exchange starts, for example because its signature is invalid,
the community cannot be loaded, or the community disabled its challenge exchange, the node still
returns a subscription id. It first sends an
errorevent whosedetails.publishThrowErroristrue, and ends the subscription.
publishCommunityList publishes a community list that the client built and signed.
communityListRawString is the exact signed JSON text, sent as a string because a re-serialized
object could change the bytes. BSIP-15 specifies what the node checks before adding
and pinning those bytes, and what the client checks in the returned cid.
Running local communities
These methods act on local communities. A node looks for a running community with the given name or
identity key, then for a stored one, matching name before publicKey in each case, so a client
that still holds a community’s old name after a rename finds it by its key.
-
createCommunitycreates a local community and returns a local community payload of the first shape. Its parameter is the community’s initial editable fields andsettings(the same members aseditOptionsbelow), and at most one of:signer: { type: "ed25519", privateKey }, a key to import as the community key;anchor: { publicKey }, making a delegated community whose minter key the node generates (BSIP-11).
With neither, the node generates the community key. The new community then appears in the communities list.
-
startCommunitystarts a local community: the node publishes its record and answers its challenge exchange (BSIP-4). If the community is already running, the subscription attaches to it. Either way the node sends anupdatewith the current state before the response. The subscription carries the community’s updates, its started state, every challenge exchange it handles (decrypted), and its errors. Several clients MAY hold start subscriptions on the same community. -
startedCommunitySubscribere-establishes a start subscription, typically after a reconnect. It attaches if the community is running, starts it if it was started and never explicitly stopped since (for example after a node restart), and otherwise fails withERR_RPC_COMMUNITY_NOT_RUNNING. It never overrides a stop. -
stopCommunitystops a running local community for every client, sends a finalupdateand astartedstatechangetostoppedon every start subscription, and records the stop so that the community is not restarted automatically. Start subscriptions receive nothing further. -
editCommunityapplieseditOptionsand returns the resulting local community payload.editOptionshas the memberstitle,description,rules,roles,features,suggested,flairs,pubsubTopic,name, andsettings, with the meanings of BSIP-3;settingsis node-side configuration that is not published, andaddressis accepted as a legacy alias ofname. A member set tonull, at any depth, is removed; inroles, anullentry removes that role. Setting or removingnamerenames the community (BSIP-9). Challenge and page-sort names insettingsrefer to the node’s registries, whichsettingschangelists (Node lists and settings). -
deleteCommunitystops the community if it runs, deletes its data, and sends the same final notifications asstopCommunity. -
prepareAnchorPublishandpublishAnchorRecordare the node’s half of setting up or rotating a delegated community (BSIP-11). The owner signs the anchor record wherever the anchor key is; that key never crosses this protocol.prepareAnchorPublishreturnsnextSequence, the sequence the owner should sign;currentAnchorRecordSequence, the highest the node or the network knows; andhasPersistedAnchorRecord, whether this node ever accepted an anchor record for the community. It fails withERR_UNABLE_TO_DETERMINE_ANCHOR_SEQUENCErather than answer 0 when no sequence is known; for the first anchor record the owner signs sequence 0 without asking.publishAnchorRecordtakes the signed IPNS record asrecordBase64. The node checks and publishes it as BSIP-11 requires, and returns itssequence, itsvalue, and theanchorPublicKeyit is for.
A method of this group fails when the identifier names no local community, with the codes listed in Errors.
Community payloads
A remote community payload is either:
{
"community": { /* the community record (BSIP-3), verbatim */ },
"runtimeFields": {
"updateCid": "Qm…",
"updatingState": "succeeded",
"nameResolved": true,
"ipnsHops": ["12D3KooW…"],
"posts": { "pages": { "hot": { "comments": [{ "author": { "nameResolved": true } }, {}] } } }
}
}
or, after the community’s name moved to a new key and the node discarded the old record (BSIP-10):
{ "resetInstance": true, "runtimeFields": { "newPublicKey": "12D3KooW…", "nameResolved": true, "updatingState": "fetching-ipns" } }
A client receiving resetInstance MUST discard the community state it holds and continue under
newPublicKey.
A local community payload has one of two shapes, depending on whether the community has published its first record:
- Before:
{ "localCommunity": { … } }, wherelocalCommunityholds the community’s fields as the node will publish them (title,challenges,encryption,createdAt,protocolVersion, and so on, withoutpostsormodQueue) together with the local members listed below. - After:
{ "community": <community record>, "localCommunity": { … }, "runtimeFields": { "updateCid", "updatingState"?, "nameResolved"?, "posts"? } }, wherelocalCommunityholds only the local members.
The local members are address; signer: { type, publicKey, address, shortAddress }, the key that
signs the community’s records (the minter of a delegated community), never with its private key;
anchor and anchorRecordSequence for a delegated community; settings; started, whether the
community is running; startedState; and _usingDefaultChallenge, whether the node applied its
default challenge because the owner configured none.
Node lists and settings
communitiesSubscribelists the node’s local communities. Itscommunitieschangepayload is{ "communities": [{ "name"?, "publicKey", "started" }] }, wherestartedis whether the community is running. The node sends the list on subscribe and whenever a local community is created, deleted, renamed, started, or stopped. A client uses it to tell local communities from remote ones.settingsSubscribereports the node’s configuration. Itssettingschangepayload is{ "pkcOptions": {…}, "challenges": {…}, "pageSorts"?: {…} }: the node’s options, and the challenges and page sorts registered on the node, keyed by name and described as their packages describe themselves, without code. A client SHOULD reject a challenge or page-sort name the node does not list before callingeditCommunity.setSettingsreplaces the node’s options withpkcOptions. The node ignores anynameResolversmember and keeps its own resolvers. If the options equal the current ones the call does nothing. Otherwise the node switches to the new options, restarts its running local communities under them, moves the existing update subscriptions to them, and sendssettingschangeto every settings subscriber. The option set is implementation-specific; this BSIP does not standardize it.
Export
These methods back up a local community’s database. The reference node’s backup is a SQLite file.
exportCommunitystarts a backup and returns itsexportId(a UUIDv4) as soon as it is queued. WithincludePrivateKey: truethe backup includes the community’s private key; a node MAY refuse this withERR_PRIVATE_KEY_EXPORT_NOT_ALLOWED. It fails withERR_COMMUNITY_NOT_FOUNDorERR_COMMUNITY_NOT_LOCALwhen the identifier names no local community.exportsSubscribereports a community’s exports. Itsexportschangepayload is{ "records": [ … ] }, each record{ exportId, name?, publicKey, includePrivateKey, progress, size?, sha256?, url?, error? }.progressruns from 0 to 1; at 1 the record carriessize,sha256(hex), andurl; a failed or cancelled export carrieserror: { code, message }, with codeERR_EXPORT_CANCELLEDfor a cancellation.urlis the relative path/exports/<exportId>, which the client resolves against the node’s HTTP origin (the WebSocket URL withwsreplaced byhttp,wssbyhttps, and the path removed). The node sends the list on subscribe and on every change. Ending the subscription does not cancel exports.cancelExportcancels an export in progress. It succeeds for an unknown or finished export.GET /exports/<exportId>returns the file with status 200 andContent-Type: application/vnd.sqlite3, 404 when there is none, and 405 for other methods. After a complete download the node deletes the file and the record. A node SHOULD delete export files that are never downloaded after a bounded time (the reference node: 24 hours, checked at startup).exportCommunityModLogsreturns the comment moderations (BSIP-8) the community accepted, as{ "moderations": [ … ] }: each is the signed moderation with node bookkeeping (insertedAt,modSignerAddress, and when knowntargetAuthorSignerAddressandtargetAuthorDomain). The filters boundtimestamp(startTimestamp,endTimestamp), select one comment (commentCid), cap the count (limit, a positive integer), and set the order by timestamp (order,"ASC"or"DESC", default"DESC").
Subscriptions
A subscription starts with a subscribe, start, or publish method, whose result is
{ "subscriptionId": n }. A subscription id is a positive integer that a JSON number holds exactly;
the reference node draws it at random from 1 to 2^48 − 2, so ids are unique in practice.
- Snapshot. A subscription that has a current state sends it at once, before the response:
communitiesSubscribe,settingsSubscribe,exportsSubscribe, and the start methods always;communityUpdateSubscribewhen the community is local or the node already holds its record;commentUpdateSubscribewhen the node already holds the comment record or its update. - Unsubscribe.
unsubscribeends a subscription of the calling connection and frees what the node holds for it. It succeeds for an unknown id. Unsubscribing a start subscription does not stop the community, and unsubscribing a publication stops it if the exchange has not ended. - Disconnect. Closing the connection ends all its subscriptions (Connection and authentication).
The events of each kind of subscription:
| Created by | Notification method | Events and payloads |
|---|---|---|
communityUpdateSubscribe | communityUpdateNotification | update: a community payload; updatingstatechange: { state }; error |
commentUpdateSubscribe | commentUpdateNotification | comment: { comment, runtimeFields? }; update: { commentUpdate, runtimeFields? }; runtimeupdate: runtime fields alone; updatingstatechange: { state }; statechange: { state }; error |
startCommunity, startedCommunitySubscribe | startCommunity | update: a local community payload; startedstatechange: { state }; challengerequest, challenge, challengeanswer: the decrypted message; challengeverification: { challengeVerification }; error |
publishComment | publishCommentNotification | challengerequest, challenge, challengeanswer: the decrypted message; challengeverification: { challengeVerification, runtimeFields? }; publishingstatechange: { state }; statechange: { state }; error |
publishVote, publishCommentEdit, publishCommentModeration, publishCommunityEdit | publishVoteNotification, publishCommentEditNotification, publishCommentModerationNotification, publishCommunityEditNotification | As publishComment, without statechange; challengeverification is { challengeVerification } |
communitiesSubscribe | communitiesNotification | communitieschange: { communities } |
settingsSubscribe | settingsNotification | settingschange: { pkcOptions, challenges, pageSorts? } |
exportsSubscribe | exportschange | exportschange: { records } |
Further rules for these events:
commentis sent once per subscription, as soon as the node holds the comment record. ItsruntimeFieldscarriesauthor.nameResolvedandcrosspostoutcomes.updatecarries the latest comment update withruntimeFieldsforauthor,crosspost, andreplies.pages.runtimeupdatecarries a changed outcome, for example{ "author": { "nameResolved": true } }, when no comment update is held to send it with.- In a
challengerequestof a start subscription, the community has decrypted the request and the publication’sauthoris a runtime author: its signed members plusaddressandpublicKey(BSIP-10) and, when the community knows the author,community, the author’s standing as the community computed it (BSIP-5).authoris present even when the publication signed none. A client that needs the signed publication removes these members, and removesauthorwhen nothing else is left in it. - A
challengeverificationthat accepted a comment carries, inchallengeVerification, thecommentrecord and its reducedcommentUpdate(BSIP-5). ForpublishComment,runtimeFields.author.nameResolvedis the node’s outcome for the author name. - On a community the node runs,
communityUpdateSubscribereports the community’s started state throughupdatingstatechange.
A client MUST ignore events it does not recognize, and MUST accept state values it does not recognize (Compatibility).
States
State events carry { "state": <string> }. The values are:
| State | Values |
|---|---|
| Community updating state | stopped, resolving-name, fetching-ipns, fetching-ipfs, succeeded, failed, waiting-retry |
| Community started state | stopped, publishing-ipns, succeeded, failed |
| Comment state | stopped, updating, publishing |
| Comment updating state | stopped, resolving-author-name, fetching-ipfs, fetching-update-ipfs, resolving-community-name, fetching-community-ipns, fetching-community-ipfs, succeeded, failed, waiting-retry |
| Publishing state | stopped, resolving-community-name, fetching-community-ipns, fetching-community-ipfs, publishing-challenge-request, waiting-challenge, waiting-challenge-answers, publishing-challenge-answer, waiting-challenge-verification, succeeded, failed |
| Publication state | stopped, publishing |
succeeded and failed in an updating state describe the last attempt; the subscription keeps
updating. waiting-retry means the node loaded a record it did not use and will try again.
Errors
An error is a JSON object with the members code, a string such as "ERR_NO_RESOLVER_FOR_NAME";
message, a human-readable description; and the OPTIONAL details, an object with values related
to the error, and name, the error class, for example "PKCError". A node SHOULD always send code
and message, and MUST NOT send stack traces. Nested errors inside details are objects with
name, message, and, when they have one, code.
Call errors. A method that fails returns a JSON-RPC error response whose error member is that
error object:
{
"jsonrpc": "2.0",
"id": 4,
"error": { "name": "PKCError", "code": "ERR_RPC_COMMUNITY_NOT_RUNNING", "message": "…", "details": { "communityAddress": "memes.bso" } }
}
The code is a string, not the integer JSON-RPC 2.0 defines. Errors produced by the JSON-RPC layer
itself keep JSON-RPC’s integer codes: -32700 parse error, -32600 invalid request, -32601 method
not found. A parameter object that fails validation produces an error whose code names the schema
(for example ERR_INVALID_COMMENT_CHALLENGE_REQUEST_TO_ENCRYPT_SCHEMA); the reference node also
returns some validation failures without a code. A client MUST accept an error object with any
code, or none, and fall back to message.
Error events. Errors that happen after a subscription started arrive as an error event whose
payload is the error object. Its details MAY carry the state the subscription moved to because of
the error, newUpdatingState, newStartedState, or newPublishingState, and publishThrowError: true when a publication ended. An error event does not end a subscription unless it says so, or the
client decides the error is not retriable.
The protocol-specific codes are:
| Code | Raised by |
|---|---|
ERR_RPC_CLIENT_ATTEMPTING_TO_START_A_REMOTE_COMMUNITY | startCommunity, startedCommunitySubscribe on a remote community |
ERR_RPC_COMMUNITY_NOT_RUNNING | startedCommunitySubscribe on a community stopped on purpose |
ERR_RPC_CLIENT_TRYING_TO_STOP_REMOTE_COMMUNITY | stopCommunity on a remote community |
ERR_RPC_CLIENT_TRYING_TO_STOP_COMMUNITY_THAT_IS_NOT_RUNNING | stopCommunity on a local community that is not running |
ERR_RPC_CLIENT_TRYING_TO_EDIT_REMOTE_COMMUNITY | editCommunity on a remote community |
ERR_RPC_CLIENT_TRYING_TO_DELETE_REMOTE_COMMUNITY | deleteCommunity on a remote community |
ERR_RPC_CLIENT_TRYING_TO_USE_ANCHOR_METHOD_ON_NON_LOCAL_COMMUNITY | the anchor methods on a remote community |
ERR_COMMUNITY_NOT_FOUND, ERR_COMMUNITY_NOT_LOCAL | the export methods on a community that is not local |
ERR_PRIVATE_KEY_EXPORT_NOT_ALLOWED | exportCommunity with includePrivateKey: true on a node that forbids it |
ERR_NO_RESOLVER_FOR_NAME | any request whose name has no resolver on the node |
Other codes come from the operation the node performed, such as loading a record, verifying a signature, or running a challenge exchange, and are defined with that operation.
Reconnection
A node keeps nothing for a dropped connection, so recovering is the client’s job. A client SHOULD:
- reconnect with a growing delay (the reference client: doubling from 1 second up to 30 seconds), indefinitely, except after a 403;
- after reconnecting, re-issue every live subscription with its original parameters, re-issuing a
start subscription with
startedCommunitySubscriberather thanstartCommunity, so that it never undoes another client’s stop; if that fails withERR_RPC_COMMUNITY_NOT_RUNNING, report the community as stopped and not retry; - rely on each re-issued subscription’s snapshot for the current state, since events during the gap are not replayed;
- not re-publish a publication that was in flight, because the node stopped it and a new exchange would present challenges the user never saw; report it as failed instead;
- treat a call that was awaiting its response when the connection closed as possibly executed, and not repeat it automatically unless it is safe to run twice;
- bound how long it waits for any response (the reference client: 300 seconds), so that a lost response surfaces as an error.
Compatibility
The protocol carries no version number and no capability negotiation. It evolves by additions only:
- A node MUST ignore unknown members of community identifiers and of the parameter objects of the
reading, subscribing,
unsubscribe,publishChallengeAnswers, export, and anchor methods. - A client MUST NOT send unknown members in the
createCommunityparameter or ineditOptions; the reference node rejects them. A client MUST NOT expect unknown top-level members of a publish method’s parameter object to reach the community; the reference node rejects them forpublishCommentand drops them for the other publish methods. The signed publication inside is carried as its BSIP allows, unknown signed members included. - A client MUST ignore unknown members of results and payloads, unknown events, and unknown error codes, and MUST accept state values it does not know.
- A new method is detected by calling it: a node that lacks it answers
-32601. - A member added to a result or payload is OPTIONAL for nodes, as
pageSortsinsettingschangeis. - A node MAY implement methods beyond this BSIP. Methods of the underlying JSON-RPC library, such as
rpc.on,rpc.off, and__listMethodson the reference node, are not part of this protocol.
Rationale
- JSON-RPC over one WebSocket. Thin clients need server push for updates and for the challenge exchange, and browsers support WebSockets everywhere. JSON-RPC adds request correlation with almost no machinery, and one connection carries both calls and notifications.
- Subscriptions, not polling. Communities and comment updates change continually. A subscription lets the node run one update loop per record, shared across clients, and push only changes.
- Snapshot on subscribe, no replay. The node keeps no per-client history, which keeps it simple and bounded. A snapshot on every subscribe makes recovery from a dropped connection a matter of subscribing again, at the cost of losing the intermediate events; clients render current state, so they rarely need those.
- Signing on the client. The author key is the user’s identity across every community. Keeping it on the client means a node, including a hosted one, can relay but never impersonate. The node still runs the challenge exchange, because that needs pubsub and an exchange key that only has to live as long as the exchange.
- Verbatim records, separate runtime fields. A record that the node annotated in place would no longer match its signature or CID, and a client could not tell what the author signed from what the node computed. Keeping the two apart lets a client verify records when it wants to and still use the node’s name outcomes, which it cannot compute without resolvers.
- Name resolution on the node. Resolution needs chain access and caching that thin clients lack. The cost is that the client inherits the node’s answers (BSIP-10).
- A path-based auth key. The browser WebSocket API cannot set request headers, so a credential has to travel in the URL. Local connections are exempt so that a desktop app and its embedded node work with no setup.
- Starts outlive connections; stops are deliberate. A community must keep publishing when the
laptop that started it sleeps or loses its connection.
startedCommunitySubscribeexists so that a reconnecting client can resume without restarting a community someone else stopped on purpose. - Out of scope. Accounts, quotas, billing, and multi-tenant authorization for hosted nodes belong
to the product that runs the node, not to the protocol. An owner publishing to a feed-only author
community (BSIP-14) uses
publishCommentlike any other publication.
Open questions:
- Error codes. Call errors use a string
codewhere JSON-RPC 2.0 requires an integer. A later version could send{ "code": -32000, "message", "data": <error object> }, which every JSON-RPC library understands, at the cost of breaking current clients. - Versioning. There is no handshake. A
rpc.discover-style method or a version member insettingschangewould let clients adapt instead of probing. - Naming. Notification
methodnames follow three patterns (startCommunity,exportschange,…Notification), andcommunityUpdateSubscribereports started states asupdatingstatechangeon a community the node runs. Both could be normalized in a breaking revision. - Scoped credentials. One key grants everything. A read-only or per-community key would let owners share a node without sharing control of it.
- Client-supplied hints. Whether a node must verify the
rawrecords a client passes tocommentUpdateSubscribe, or may use them as given.
Backwards Compatibility
This is the first BSIP for the RPC protocol; it specifies the protocol as pkc-js v0.0.135 implements
it. The protocol evolved without a version number, so peers from older releases can disagree on
shapes. Notable changes: the communities list carries { name?, publicKey, started } entries since
v0.0.129 and plain addresses before it, which current clients reject with
ERR_INVALID_RPC_COMMUNITIES_SCHEMA; startedCommunitySubscribe exists since v0.0.117; page and
comment methods take communityPublicKey and communityName instead of a single address. A client
talking to an older node can detect a missing method by its -32601 error and a missing optional
member by its absence.
The pkc-js README for the RPC predates several of these changes and differs from this BSIP; where they disagree, this BSIP describes the implementation.
Reference Implementation
Non-normative. The reference implementation is pkc-js:
- the node,
PKCWsServer, insrc/rpc, mainlysrc/rpc/src/index.ts, with the challenge-message encoding insrc/rpc/src/utils.ts, built on rpc-websockets; - the client in
src/clients/rpc-client/pkc-rpc-client.ts, with parameter and result schemas insrc/clients/rpc-client/schema.ts; - the client-side community and publication objects in
src/community/rpc-remote-community.ts,src/community/rpc-local-community.ts, and_publishWithRpcinsrc/publications/publication.ts; - the export design in
src/rpc/EXPORT_COMMUNITY_SPEC.md.
A pkc-js instance becomes a client of this protocol when it is created with pkcRpcClientsOptions
set to a node’s URL.
Security Considerations
- The client trusts its node for everything it reads. A node can return forged, stale, or
incomplete records, hide comments or communities, report any
nameResolvedoutcome, and report a challenge verification that never happened. The reference client verifies none of these. A client that needs assurance MAY check what it receives: recompute CIDs and verify signatures as BSIP-16 describes, usingfetchCidfor exact bytes. Verification exposes forgery, not withholding. - What the node cannot do with publications. It never holds the author’s private key, so it cannot sign a new publication as the author, change a signed one, or redirect it to another community without breaking its signature. It can drop or delay a publication, and replay it only within the limits of BSIP-4 and the timestamp window. It does see the publication in plaintext before encrypting it, along with the challenges and answers.
- The record the client signs against. A client that learns a community’s identity key from its node signs whatever key the node reports. A malicious node can steer the client’s publication to a community the client did not mean. Clients SHOULD confirm a community’s key through a second source before publishing anything sensitive to it.
- Community keys. A node holds the key of every community it runs, or the minter key of a
delegated one; BSIP-11 limits what a hosted node can do with the community’s
identity.
createCommunitywithsignersends a private key to the node, andexportCommunitywithincludePrivateKey: truesends it to any client of the node. A node shared with anyone the owner would not give the community key to SHOULD refuse private-key exports. - One credential for the whole node. The auth key grants every method, including
setSettings, which can point the node at providers of the caller’s choosing, and the export of private keys. It travels in the URL path, so it can appear in proxy logs and browser history, and anyone who can read aws://connection learns it.settingschangeexposes the node’s options, and local community payloads exposesettings, including challenge options that may be secrets, to every connected client. - Local connections are unauthenticated. Any process on the node’s host can connect without the
key, and so can a local reverse proxy that does not add
X-Forwarded-For. Browsers generally let a web page open a WebSocket tolocalhost, so a page the user visits can also reach a node on the user’s machine. A node SHOULD reject upgrade requests whoseOriginheader names a web origin it does not trust unless they present the auth key; the reference node does not checkOrigin. - Export URLs are credentials.
GET /exports/<exportId>takes no auth key. The 122-bit random export id is the only protection, so it MUST be treated as a secret, especially for exports that include a private key. Deleting files after download and after a bounded time limits exposure. - Answers across connections. The reference node looks up the publication named in
publishChallengeAnswerson every connection, relying on the subscription id being unguessable. A node SHOULD accept answers only from the connection that started the publication. - Resource use. Each subscription makes the node fetch, resolve, and verify on the client’s behalf, and the reference node sets no limit per connection. A node exposed to untrusted clients SHOULD bound subscriptions per connection and the rate of publish calls.
- Privacy. The node learns the client’s network address, everything it reads and follows, and everything it publishes, with timing. In exchange, peers on the pubsub and IPFS networks see the node, not the client. A hosted node serving many users becomes a point where their activity can be correlated; users who need to avoid that should run their own node.
Copyright
Copyright and related rights waived via CC0.