On this page
- Abstract
- Motivation
- Specification
- Terminology
- The list record
- Entries
- The author field
- Runtime values and reserved names
- Signing
- Addressing
- Size limit
- Publishing a list
- Loading and verifying a list
- Using entries
- Revisions
- Publishing and loading through an RPC server
- Rationale
- Backwards Compatibility
- Reference Implementation
- Security Considerations
- Copyright
Abstract
A community list is a signed, immutable JSON record, stored on IPFS and addressed by its CID, in
which a curator lists communities together with metadata the curator assigns to each: a name, a
title, a description, languages, locations, features, and tags. Clients load lists to render default
feeds and to search communities by that metadata without loading each community. This BSIP defines
the record and its entries, the curator’s optional author profile, how the record is signed (every
member except signature), the names it reserves for runtime values, its 2 MiB size limit, how a
list is published, and how a reader loads and verifies it by CID. A revision is a new record with a
new CID, and clients never move from one revision to another on their own. The format replaces the
earlier “multisub” design, which was never implemented; that name is retired.
Motivation
Clients need sets of communities to start from: a front page, a topic feed such as crypto, or a
large, loosely curated index to search. Reading the title and topic of every community from its own
record costs at least one IPNS resolution and one fetch per community (BSIP-3), and
tells the network which communities the reader is looking at. A list carries enough metadata for a
client to filter and search locally, and to load only the communities it actually shows.
A list is also a trust decision. Apps ship list addresses as defaults, so whoever controls a list decides where every installed client goes. Independent clients need a shared format in which a list is attributable to a key, cannot be altered by whoever relays it, and cannot change after a client adopted it.
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
- List — a community list record as defined in this BSIP. Implementations call the record
CommunityListand an entryCommunityListEntry. - Curator — the holder of the key that signs a list.
- Entry — one element of a list’s
communitiesarray, describing one community. - Revision — a list a curator publishes to supersede an earlier one. It is a separate record with its own CID.
The list record
A list is a JSON object with the following members:
title— OPTIONAL. String. The list’s display title.description— OPTIONAL. String.author— OPTIONAL. The curator’s profile (Theauthorfield).communities— REQUIRED. Array of entries, possibly empty. Two entries MUST NOT have the samepublicKey, compared as exact strings.timestamp— REQUIRED. Positive integer Unix time in seconds: when the curator signed this revision.protocolVersion— REQUIRED. Non-empty string, currently"1.0.0".signature— REQUIRED. The curator’s signature (Signing).
Strings and arrays have no individual length limits; the whole record is bounded by the size limit. Every array-valued member, of the list and of its entries, has a plural name.
Example:
{
"title": "Funny communities",
"description": "The funniest communities",
"author": { "name": "john.bso", "displayName": "John" },
"communities": [
{
"publicKey": "12D3KooWNsF2eMrdw3YFPycAaFEvZ9Eq3ekhNL3vrAAfN6dAwXLp",
"name": "funny.bso",
"title": "Funny things",
"languages": ["en"],
"tags": ["funny", "memes"]
},
{ "publicKey": "12D3KooW…" }
],
"timestamp": 1792713600,
"protocolVersion": "1.0.0",
"signature": {
"type": "ed25519",
"signature": "<base64>",
"publicKey": "<base64, 32 bytes>",
"signedPropertyNames": ["title", "description", "author", "communities", "timestamp", "protocolVersion"]
}
}
Entries
An entry describes one community. Its metadata is set by the curator, not by the community’s owner, and nothing in it is signed by the community.
publicKey— REQUIRED. Non-empty string: the community’s identity key as a key address (BSIP-10), the value publications carry incommunityPublicKey(BSIP-3).name— OPTIONAL. Non-empty string: the community’s name (BSIP-10), for examplememes.bso.title— OPTIONAL. String: the curator’s title for the community.description— OPTIONAL. String: the curator’s description of the community.languages— OPTIONAL. Array of strings: the languages of the community, so that a client can show or hide it by the reader’s language.locations— OPTIONAL. Array of strings: the locations the community concerns, so that a client can show or hide it by the reader’s location.features— OPTIONAL. Array of strings: properties of the community that a client can match against the reader’s settings, for example whether it is safe for work. It is unrelated to thefeaturesobject of the community record (BSIP-3).tags— OPTIONAL. Array of strings: keywords for search.
This BSIP defines no vocabulary for languages, locations, features, or tags; their values are
free-form strings. Readers check publicKey and name only as non-empty strings: neither the
key-address form nor the name syntax is validated when a list loads.
The author field
author is the curator’s profile, which clients show next to the list the way they show a
publication’s author next to a comment. It has the shape of a publication’s author
(BSIP-2): the OPTIONAL members name, displayName, previousCommentCid,
wallets, avatar, and flairs, with the same types. Other members MAY be present and are signed as
part of author.
author.name, when present, MUST be a name, that is, contain a.(BSIP-10).authorMUST NOT carryaddress,publicKey,shortAddress,nameResolved, orcommunity. A list is never sent to a community, so a list author has no community standing (author.community) on the wire or at runtime.authorcarries no identity member. The curator’s identity issignature.publicKey.- A curator without profile members omits
author; signing drops an empty object (BSIP-2).
Clients derive the runtime author as for publications
(BSIP-10): author.publicKey is the signer address, and
author.address is author.name when present, otherwise author.publicKey. A client MUST NOT present
author.name as the curator’s identity unless it verified the name for the signer
(BSIP-10). The outcome of that verification does not affect
whether the list is valid.
Any key can sign a list, including the key the curator signs publications with; a list signed with that key carries the same verifiable author identity as the curator’s comments. One key can sign any number of lists.
Runtime values and reserved names
Implementations derive the following values. They are not part of the wire record and are never signed:
- on the list:
cidandshortCid; - on each entry:
address, equal tonamewhen present, otherwisepublicKey, andshortAddress; - on
author: the values listed in Theauthorfield.
A list MUST NOT carry a top-level member named cid, shortCid, state, updatingState,
publishingState, clients, signer, or raw. An entry MUST NOT carry address or
shortAddress. author MUST NOT carry the names listed in The author field.
Members added by later BSIPs MUST NOT use these names.
Signing
The curator signs a list following BSIP-2, Signatures. signedPropertyNames
MUST list every top-level member of the record except signature, including members a reader may not
recognize. This is stricter than the rule for comment records, which may carry unsigned community-set
and unknown members (BSIP-2). Readers apply this rule to the record as
received, before verification narrows the record to its signed members
(Loading and verifying a list). Members of entries and of author
need no separate check: communities and author are each signed as a whole.
Readers MUST accept and preserve members they do not recognize, at the top level, in entries, and in
author, and MUST include them when verifying the signature. A newer publisher may add and sign
members a reader does not know; a reader that dropped them would fail to verify the list.
Addressing
A list is identified by its CID alone. It has no IPNS name, no name of its own, and no member that points to another list.
- The CID is computed over the exact UTF-8 bytes of the record as published, imported as
BSIP-2, Addressing and fetching describes in its step 3, and
expressed as a CIDv0 (
Qm…). - Unlike a comment record, a list has no canonical serialization. A publisher MAY serialize the record as any JSON text. The same signed list serialized another way has another CID and verifies under both.
- A list is referenced by its CIDv0 string. Implementations MUST reject a CIDv1 given as the address of a list.
Size limit
The size limit of a list is 2,097,152 bytes (2 MiB).
- A publisher MUST NOT publish a list whose DAG size exceeds the limit. The DAG size is the total
size of the blocks produced by importing the record’s bytes as in
BSIP-2, Addressing and fetching, which
ipfs addreports as the file’s cumulative size. - A reader MUST reject a list whose bytes exceed the limit, before parsing it, and SHOULD stop downloading once more bytes than the limit have arrived.
The DAG size of a file always exceeds its byte length, so every list a conforming publisher produces passes the reader’s check. A list is a single file; there is no pagination or chunking.
Publishing a list
To publish a list, a curator:
- builds the record from the members defined above and removes, at every depth, members whose value
is
null, absent, or an empty object, as signing requires (BSIP-2); - signs it (Signing);
- serializes it as JSON, encodes the text as UTF-8, and checks the size limit;
- adds the bytes to IPFS as a CIDv0 file and keeps them available, for example by pinning them and announcing them to the network;
- distributes the resulting CID.
A publisher MUST NOT publish a list that fails any check in Loading and verifying a list.
The curator keeps the signing key; no other party holds it or signs on the curator’s behalf. Losing the key does not affect published lists, whose CIDs stay valid, but prevents signing later revisions under the same identity. There is no edit publication: nobody, the curator included, can change a published list.
Loading and verifying a list
A reader loads a list from its CIDv0 in the following steps, and MUST reject the list if any step fails:
- Fetch the bytes by CID from IPFS peers or gateways (BSIP-16), enforcing the size limit, and check that the bytes hash to the CID (BSIP-2).
- Parse them as JSON.
- Check the presence and types of the members defined in The list record,
Entries, and The
authorfield, keeping members it does not recognize (Signing). - Check that:
- no reserved name appears at the top level, in
author, or in an entry; author.name, if present, contains a.;- no two entries have the same
publicKey; - every top-level member other than
signatureis listed insignedPropertyNames; and - the signature verifies (BSIP-2).
- no reserved name appears at the top level, in
The reader then derives the runtime values. A list that fails a
step is invalid as a whole; a reader MUST NOT accept part of it, for example by dropping an invalid
entry. Readers do not compare timestamp with the current time.
A failure in steps 2 to 4, or a record over the size limit, is a property of the bytes the CID names, so fetching the same CID again cannot fix it. A reader SHOULD NOT retry such a failure, and SHOULD retry transport failures.
Verifying author.name (The author field) is not part of loading. A reader
MAY return a list before that verification completes and report the outcome when it does.
Using entries
A list is a directory of pointers, not an authority. Entry metadata is the curator’s claim about a community.
- A client MAY render, filter, and search entries by their metadata without loading the communities. It MUST NOT present entry metadata as the community’s own or as signed by the community.
- A client MAY show or hide entries by matching
languages,locations, andfeaturesagainst the reader’s language, location, and settings. Such filtering applies the curator’s labels; it is not a property the client has verified. - A client opens an entry by loading the community by its
publicKeyor itsname, as BSIP-3 and BSIP-10 specify, which verifies a name against the community’s key at that point. A client MUST NOT present an entry’snameas the community’s verified name on the strength of the list alone. - Loading a list resolves no entry names. A client SHOULD load and resolve only the entries it displays, not a list’s entries in bulk.
Revisions
A revision is a new list with a new CID. A list carries no reference to earlier or later revisions. To revise a list, the curator builds a new record, typically from the members of the loaded current revision without its runtime values, and publishes it.
A client MUST NOT replace the list it uses with another revision unless its user or its app configuration chooses the new CID. In particular, a client MUST NOT follow a reference from one revision to another found inside a list, should a later BSIP define one.
How a curator announces a new revision, and which lists a client uses by default, are out of scope. Default lists are app configuration.
Publishing and loading through an RPC server
A client connected to an RPC server (BSIP-17) publishes and loads lists through two methods. The signing key never leaves the client, and the server is not trusted to judge validity.
publishCommunityList, with the parameter{ "communityListRawString": string }: the exact bytes the client signed and serialized. The server checks the size limit, parses and verifies the record as a reader would, refuses one that fails, adds the exact bytes to IPFS, pins them, and returns{ "cid": string }. The client MUST check that the returned CID is the CIDv0 of the bytes it sent, and MUST treat any other CID as a failed publish.fetchCommunityList, with the parameter{ "cid": string }: the server fetches the bytes under the size limit and returns them unparsed as{ "communityListRawString": string }. The client MUST enforce the size limit, check that the bytes hash to the CID, and parse and verify the list itself.
Rationale
- Immutable records instead of IPNS. The original design (pkc-js issue #21) published lists as mutable IPNS records. It was reversed on 2026-09-06 for a security reason: apps ship list addresses as defaults, so a mutable list is a permanent, unreviewable delegation of trust to one keyholder. A single IPNS update could send every installed client to arbitrary new communities, illegal content included, with nobody in the loop, and because a list update swaps the whole set of places a client goes, the damage of one update is large. With immutable CIDs, whoever adopts a CID decides once, over concrete bytes, and each revision is reviewed again when it is adopted. That is also why clients never move between revisions on their own. Immutability also removes the IPNS machinery: republishing, liveness, delegated publishing, staleness rules, and sequence numbers.
- Every member signed. A comment record has a second producer, the community, which legitimately adds unsigned members (BSIP-2). A list has a single producer, so an unsigned member can only have been added by someone else. Without the rule, anyone could add a member to a curator’s list, add the result to IPFS under a new CID, and have it verify as the curator’s.
- Loose loading. Verification walks the record’s own
signedPropertyNames. A reader that rejected or dropped unknown members would reject lists from newer publishers or break their signatures, so readers keep unknown members and require only that top-level ones be signed. New optional members, such as a revision reference, can therefore be added later without breaking existing readers. - Reserved names. Because loading is loose, a record could otherwise smuggle in names that implementations use for runtime values. Every other record type follows the same rule.
- CIDv0 only. Every content check in the reference implementation (over RPC, through gateways, and in the size check of the kubo path) recomputes a CIDv0. Accepting a CIDv1 would make a list load over some transports and not others.
- One file and a 2 MiB limit. At roughly 200 bytes per entry, a list of about ten thousand entries fits in one file, which keeps fetching and verification simple. Readers enforce the limit as well as publishers, because a limit only honest publishers observe does not bound what readers download. Chunking can be added if a real list outgrows one file.
- No name resolution for entries. A list can hold thousands of entries; resolving every name on load would be a burst of resolver traffic, and the community load path already verifies a name against its key when a client opens the community.
author.namemust be a name.author.addressisauthor.namewhen present. A name without a., such as another key’s address, would make the list display as authored by that key. Comments pass through a community that rejects such names before anyone sees them; a list reaches readers directly, so the reader is the only place to reject them.- The publication author schema. Reusing the
authorof publications lets clients render a curator with the code that renders comment authors, and a list signed with the curator’s personal key shows the same identity as their comments. Signing a list uses no IPNS name, so one key can sign any number of lists. - No edits and no shared custody. Only the keyholder signs. An edit publication signed by others
(the earlier
MultisubEdit) contradicts that, and a revision already covers the curator’s own changes. - No registry of default lists. Which lists an app starts from is a product decision, so it
stays in app configuration rather than in the protocol (the earlier
PKCDefaultsidea). - Retiring “multisub”. The name was a remnant of the “subplebbit” vocabulary, which the protocol
renamed to “community”. There is no
Multisub,MultisubCommunity, orMultisubEditrecord.
Open questions:
- A signed reference to the previous revision (for example
previousCommunityListCid) is deferred. It would let clients show a list’s history, never follow it automatically. - A curator’s channel for announcing revisions is undefined. The curator’s author community (BSIP-14), or another community the curator runs, is the expected channel; both are mutable, and a community is moderated.
- Whether
languages,locations, andfeaturesshould use defined vocabularies, for example BCP 47 language tags and ISO 3166 codes, so that independent clients filter alike. - Whether readers should reject an entry
namethat is not a name, and anauthor.namewith an uppercase ASCII letter, as BSIP-10 requires of names generally. The reference implementation checks only thatauthor.namecontains a.and does not check entry names. - Whether the order of entries carries meaning, for example a ranking.
- How the reference implementation measures size on load. Its direct paths count the file’s bytes, as specified above, but its RPC client measures the DAG size. Over RPC it therefore rejects lists whose byte length is within about 500 bytes of the limit, which only a non-conforming publisher produces.
Backwards Compatibility
Lists are a new record type; no existing record changes. The earlier “multisub” design (lists
published over IPNS, with a names[] field, magnet URIs, MultisubEdit publications, and a
PKCDefaults registry) was documented but never implemented, so there are no records in that format
to migrate. Default lists that apps already ship as their own configuration are not lists under this
BSIP and are unaffected.
Readers that predate this BSIP cannot load lists. Because readers keep unknown signed members, lists that later BSIPs extend with new optional members remain loadable by readers of this version.
The reference implementation, pkc-js, has supported lists since v0.0.125. This BSIP describes its behavior as of v0.0.135.
Reference Implementation
Non-normative. The current implementation is in pkc-js:
schemas, reserved names, and the size limit in
src/community-list/
(schema.ts), publishing, loading, retries, and author name verification in
src/community-list/community-list.ts, signing and verification in src/signer/signatures.ts
(signCommunityList, verifyCommunityList), and the RPC server methods in src/rpc/src/index.ts.
The implementation notes are in
docs/protocol/community-lists.md.
The API is pkc.createCommunityList({ signer, title, description, author, communities, timestamp })
followed by publish(), and pkc.createCommunityList({ cid }) followed by update(), or the
one-shot pkc.getCommunityList({ cid }). Creation rejects unknown members, so a typo is never signed
into an immutable record; it drops the runtime values a loaded list carries, so a revision can be
built from a loaded list. Publishing needs a kubo RPC client or a PKC RPC server; instances that use
only gateways or libp2p-js can load lists but not publish them. author.nameResolved is true when
the name is verified, false when it is refuted or when no configured resolver handles its TLD (which
BSIP-10 classifies as unknown, not refuted), and absent until
settled.
Rejections and their pkc-js error codes:
| Check | Error code |
|---|---|
| Address is not a CIDv0 | ERR_INVALID_CREATE_COMMUNITY_LIST_OPTIONS_SCHEMA |
| Record over the size limit, on load | ERR_OVER_DOWNLOAD_LIMIT |
| Record over the size limit, on publish | ERR_COMMUNITY_LIST_OVER_ALLOWED_SIZE |
| Bytes do not hash to the CID | ERR_CALCULATED_CID_DOES_NOT_MATCH |
| Not JSON | ERR_INVALID_JSON |
Wrong member types, or duplicate entry publicKey | ERR_INVALID_COMMUNITY_LIST_SCHEMA |
Reserved name, author.name without ., unsigned member, or invalid signature | ERR_COMMUNITY_LIST_SIGNATURE_IS_INVALID, with the specific reason |
| RPC server returned another CID on publish | ERR_ADDED_COMMUNITY_LIST_TO_IPFS_BUT_GOT_DIFFERENT_CID |
Security Considerations
- Adopting a list is a trust decision. A client that uses a list goes where its curator points. Immutability limits that trust to bytes someone reviewed: a curator, or an attacker holding the curator’s key, cannot change a list a client already uses. A compromised key can still sign new lists that look like the curator’s, which is why moving to a new CID needs a user’s or app developer’s choice.
- Curator claims. Every entry member,
nameincluded, is asserted by the curator, not by the community. A list can mislabel a community’s language, location, orfeatures, for example labelling an adult community as safe for work. Clients MUST NOT treat such labels as a content guarantee, and SHOULD apply their own policy to the communities and content they display. - Name spoofing. An entry’s
nameand the list’sauthor.nameare claims until verified for the corresponding key (BSIP-10). A list can pair a well-known name with an attacker’s key; a client that opens the entry by thatnamereaches the community the name actually resolves to, and a client that opens it bypublicKeyhas verified nothing about the name. - Republication and old revisions. The CID identifies bytes; the signature identifies the
curator. Anyone can add a curator’s list to IPFS again, or serialize it differently under another
CID, and it remains the curator’s signed content. Published revisions never expire or become
revocable, so anyone can keep presenting an old revision, for example one that still lists a
community the curator later dropped.
timestampis the curator’s own claim and does not establish which of two revisions is newer. - Denial of service. Readers enforce the size limit while downloading and before parsing, and loading a list triggers no name resolution and no per-entry fetch. A list can still name thousands of communities, which is why clients load and resolve only the entries they display (Using entries).
- RPC servers. A server cannot forge a list, because the client checks the CID and verifies the signature itself, and it cannot substitute other bytes on publish, because the client checks the returned CID. It can refuse service, and it learns which lists a client publishes and fetches. A server pins any validly signed list up to the size limit for any client allowed to call it, so access to the method is the server’s control against abuse (BSIP-17).
- Availability. A list is retrievable only while some node that holds it serves it. Losing the curator’s key loses nothing already published, but a list nobody serves cannot be loaded; apps that ship a list as a default have to keep it served.
- Privacy. Fetching a list by CID tells the peers or gateways asked that the reader wanted it. Searching and filtering a list locally reveals less than loading every community to read its metadata, which is part of the purpose of lists.
Copyright
Copyright and related rights waived via CC0.