Signed-off-by: cyberpunk <d6839277d8e2b34d4f50da40d4de079dc36df4c282e87147aaaa2674c070cf5a@buzz.block.builderlab.xyz>
23 KiB
NIP-CL
Channel Labels
draft optional relay
Abstract
This specification defines bounded, shared string labels for Buzz stream and forum channels. Clients create initial labels with kind 9007, add or remove labels with kind 9002, and read the authoritative label set from relay-signed kind 39000 channel metadata. Ordinary NIP-01 filters discover authorized channels by label, independently of channel type.
This NIP also defines atomic application and retry behavior for all kind 9007 creation commands that include h, including unlabeled creation. It does not change the contract for unlabeled creation without h.
The commands extend NIP-29. Snapshots use NIP-32 self-labeling: l tags in the single fixed nip-cl namespace label the metadata event itself. This NIP defines that event as the authoritative projection of the channel's shared label set. It introduces no kind 1985 label events or third-party label authority.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as normative requirements.
Scope
Labels are channel metadata, not per-user organization. They MUST NOT change channel identity, history, Canvas, membership or permissions. Label values and namespace-like prefixes confer no authority. This specification reserves no values or prefixes and assigns no feature-specific behavior to them.
Version 1 applies only to stream and forum channels. Relays MUST reject initial labels and label mutations for all other channel types, including direct-message channels. Supporting labels on another channel type requires an explicit extension of this scope.
Archived channels retain readable labels but MUST reject new label mutations. A client must unarchive the channel separately before submitting a new mutation. Deleted channels MUST NOT accept label mutations or receive new metadata snapshots. A deleted channel UUID MUST NOT be reused for creation.
Replace-all operations, consumer interfaces, creator identity tags, immutable channel creation times, duplicate-channel selection and system-channel behavior are outside this specification. Label discovery within the reader's authorized community catalog is in scope; it does not guarantee uniqueness or identify who created a channel.
Capability discovery
A supporting relay MUST advertise nip-cl in its NIP-11 supported_extensions string array, not in supported_nips:
{
"supported_extensions": ["nip-cl"]
}
Clients MUST check this capability before submitting labeled creation or label mutation commands, relying on label discovery, or relying on this NIP's acknowledgement guarantees for unlabeled h-tagged creation. Relays MUST independently enforce whether these operations are enabled. Advertisement alone is not an enforcement mechanism.
A relay MUST NOT advertise support until all serving command and metadata writers and discovery readers preserve the guarantees in this specification, including canonical snapshots for existing labeled channels.
Label values and limits
A label value MUST be a nonempty ASCII string matching the complete pattern:
^[a-z0-9][a-z0-9._:/-]*$
Each value MUST be at most 64 bytes. Relays MUST reject noncanonical values rather than trimming, lowercasing, normalizing or otherwise rewriting them. Clients MUST preserve valid values they do not recognize.
The following limits apply:
| Limit | Maximum |
|---|---|
| Labels stored on one channel | 32 |
| Bytes in one label value | 64 |
| Raw label tags in one creation or mutation command, before deduplication | 64 |
For creation, the raw count covers label tags. For mutation, it covers add-label and remove-label tags combined. Each of these command tags MUST contain exactly two elements: its tag name and its value. Snapshot l and L tags have the shapes specified below.
Existing whole-event and transport-frame limits also apply. A relay MUST reject a command whose resulting snapshot would exceed an applicable limit before applying the command.
Events
| Purpose | Kind | Channel identifier | Label tags |
|---|---|---|---|
| Create a channel with initial labels | 9007 |
h |
label |
| Add or remove labels | 9002 |
h |
add-label, remove-label |
| Read authoritative channel metadata | 39000 |
d |
L, l |
Creation and mutation commands are signed by the requesting author. Metadata snapshots are signed by the relay. Existing envelope, authentication and channel-creation requirements continue to apply. Relays MUST reject add-label or remove-label on kind 9007, label on kind 9002, and l, L or t on either command kind. These are misplaced tags, not alternative label formats; rejection MUST leave all command effects unapplied.
An h-tagged creation or label mutation command MUST contain exactly one h tag with exactly two elements: h and a valid, non-nil channel UUID. Duplicate or malformed channel identifiers, and missing identifiers where required, MUST be rejected before command application. Labeled creation without h MUST be rejected.
The examples below are event fragments. Normal event fields, signatures and any required authentication tags are omitted.
Initial labels: kind 9007
Initial labels are expressed as repeated label tags alongside the ordinary creation fields:
{
"kind": 9007,
"tags": [
["h", "42fdbd94-13e6-4f82-b065-5812e4a02a8b"],
["channel_type", "stream"],
["name", "Build Systems"],
["label", "team:infra"],
["label", "workflow/build"]
],
"content": ""
}
Repeated values MUST be deduplicated in the applied set, without rewriting the signed command. Omitting initial labels creates an empty label set. Initial fields, labels, owner membership and the authoritative metadata snapshot MUST be committed together for the h-tagged creation path, whether or not labels are present. Rejection MUST leave no newly created channel or owner membership.
After the retry admission checks below, an exact retry of a committed creation command MUST be recognized before treating an existing channel as a creation conflict. A different creation event targeting an existing or deleted UUID MUST be rejected as a conflict. It MUST NOT disclose the channel's metadata.
Add and remove labels: kind 9002
A label mutation MUST contain at least one add-label or remove-label tag:
{
"kind": 9002,
"tags": [
["h", "42fdbd94-13e6-4f82-b065-5812e4a02a8b"],
["add-label", "status:active"],
["remove-label", "status:queued"]
],
"content": ""
}
A label mutation MUST be label-only. It MUST NOT also change name, about, visibility, archive state, topic, purpose or TTL. Required envelope and authentication tags remain permitted. Malformed or unknown-tag handling MUST NOT allow a label operation to bypass privileged metadata authorization.
Let L be the current label set, A the distinct values of all add-label tags, and R the distinct values of all remove-label tags. The relay MUST reject the command if A and R intersect. Otherwise, the resulting set is:
L' = (L ∪ A) ∖ R
Adding an existing value or removing an absent value succeeds as a no-op, subject to the same validation and authorization as a state-changing command. A new no-op command MUST record committed application but reuse the existing authoritative snapshot without changing its ID or timestamp. It MUST NOT publish a new snapshot solely for the no-op. A missing or stale snapshot MUST be repaired under the publication guarantees below before acknowledging success.
The resulting set, not the intermediate order of tags, determines the stored-label limit. Operations MUST preserve labels not named by the command. Ordinary metadata commands that omit label operations MUST preserve the current label set.
Metadata snapshot: kind 39000
The relay publishes the complete current label set as NIP-32 l tags, each marked with the nip-cl namespace:
{
"kind": 39000,
"tags": [
["d", "42fdbd94-13e6-4f82-b065-5812e4a02a8b"],
["t", "stream"],
["name", "Build Systems"],
["L", "nip-cl"],
["l", "status:active", "nip-cl"],
["l", "team:infra", "nip-cl"],
["l", "workflow/build", "nip-cl"]
],
"content": ""
}
A nonempty label set MUST have exactly one two-element ["L", "nip-cl"] tag and one three-element ["l", <value>, "nip-cl"] tag per value. The l tags MUST be deduplicated and sorted lexicographically by ASCII value. An empty set MUST omit both L and l tags. Snapshots MUST NOT contain other L namespaces, unmarked or differently marked l tags, or legacy label tags. Clients MUST discard a noncanonical label representation rather than partially accepting it or treating it as empty.
This single-namespace rule is deliberate: NIP-01 indexes only the first value of a tag. #L and #l match independently; they cannot require the namespace mark of a particular l tag. Allowing other namespaces on the same snapshot could therefore produce false label matches.
The existing channel_type creation tag retains its meaning. A snapshot MUST contain exactly one two-element t tag identifying its channel type; t MUST NOT carry labels. A value such as stream is a valid label in l and does not change the channel type.
Clients MUST read labels from authorized, relay-signed channel metadata, not infer authoritative state from stored command events. Before accepting a snapshot, a client MUST verify its NIP-01 event ID and signature, and require its signer to match the relay identity obtained from the configured relay's authenticated NIP-11 self field or an explicitly trusted out-of-band identity. A valid signature from an arbitrary key is insufficient. The snapshot MUST identify the expected channel with exactly one two-element d tag and contain a complete canonical label set within the limits above; otherwise the client MUST discard it, not interpret it as an empty set.
Clients MUST scope cached snapshots to the community and relay identity, and select replacements using NIP-01 ordering: greatest created_at, then lowest lexicographic event ID on a tie. An older snapshot arriving later MUST NOT roll labels back.
Label commands and metadata snapshots are state events, not conversation messages. Receiving them MUST NOT increase chat or reply counts, add chat unreads, mark a conversation read, or generate mention or push notifications from their tags.
Finding channels
Supporting relays MUST match ordinary NIP-01 #L and #l filters on the current relay-signed kind 39000 snapshots. These filters compose with the existing #t channel-type and #d channel-ID filters. Exact channel reads and existing ACL-filtered discovery MUST include the same canonical labels. No custom #label filter or new discovery endpoint is introduced.
For example, this filter finds authorized stream channels labeled team:infra:
{
"kinds": [39000],
"#t": ["stream"],
"#L": ["nip-cl"],
"#l": ["team:infra"],
"limit": 50
}
To find either of two labels, irrespective of channel type within this NIP's scope:
{
"kinds": [39000],
"#L": ["nip-cl"],
"#l": ["team:infra", "workflow/build"],
"limit": 50
}
Values within one filter field combine with OR; different fields combine with AND; multiple filter objects combine with OR. #l: ["a", "b"] therefore does not require both labels. Clients needing an intersection can query candidates and inspect their complete label sets. authors, when supplied, identifies the relay signer, not the channel creator.
Relays MUST select current snapshots, enforce current metadata-read authorization and match all filter predicates before applying limit, using NIP-01 result ordering. Nonmatching, superseded or inaccessible snapshots MUST NOT consume the result limit. Fetching only the newest unfiltered page and then filtering it is not conformant: an older matching channel must not disappear behind newer nonmatching channels. The same rule applies through the relay's generic HTTP query bridge; this NIP does not add another read-authorization path.
Live delivery remains subject to existing subscription scope, current ACLs and filter matching; this NIP does not promise a global live metadata feed or a complete catalog from one limited query. When a channel loses a queried label, its replacement snapshot no longer matches that filter, so the subscription is not a removal feed. Clients maintaining a catalog MUST reconcile retained entries through authorized exact-channel reads or broader subscriptions. Filtering a partially loaded catalog does not establish relay-wide completeness.
Labels remain mutable shared metadata, not unique IDs or creator provenance. An empty result MUST NOT be treated as proof that no same-labeled channel exists outside the returned query scope, limits or access rights. Two distinct creation commands may create different channels with the same labels; this NIP's exact-event retry guarantee does not deduplicate those channels.
Authorization and visibility
Creation and label-only mutation require channels:write and the existing identity and community admission checks.
New label mutations require channel owner/admin authority or the existing exception for the owning human of an active owner agent. Under that exception, the human need not independently be a channel member. Ordinary membership alone is insufficient. Authorization and channel lifecycle state MUST be checked against the state with which the command is serialized, not against an earlier cached role or channel read.
Any authorized label writer can add or remove any valid value; a prefix does not give its creator exclusive control. An agent that labels a channel therefore needs the same authority as any other label writer.
Labels have exactly the same visibility as channel metadata. Open-channel labels are visible to community members permitted to discover that channel. Private-channel labels remain subject to the channel ACL. Labels MUST NOT weaken tenant isolation or bypass discovery restrictions.
Label state, application evidence, serialization, caches and delivery MUST be scoped to the community resolved from the connection's host, as described in the multi-tenant relay contract. The same signed event submitted in another community MUST NOT recover evidence from the first community.
Application and publication guarantees
A successful acknowledgement for a new command means committed application, not merely storage of the signed request.
For h-tagged creation and state-changing label mutation, the relay MUST atomically commit the signed command, its application evidence, channel changes and authoritative signed 39000 snapshot. If application fails, none of those changes may commit. A new no-op command commits the signed command and its application evidence while preserving the authoritative snapshot as specified above. This guarantee does not require atomic publication of kinds 39001 and 39002 or successful delivery of every live notification.
A transactionally stored command MAY itself serve as application evidence if its provenance and retention satisfy this NIP. No separate receipt table is required. Mere presence of a stored command without proof of atomic application MUST NOT be treated as success or permission to replay its effects.
Concurrent mutations MUST serialize against authoritative channel state. Independent additions or removals MUST preserve unrelated values. State limits, authorization, archive state and deletion MUST be checked within the same serialization boundary.
Every metadata publisher, including ordinary metadata updates and repair, MUST preserve the canonical labels. A delayed publisher MUST NOT restore stale labels, including after the final label has been removed. A newly published replacement MUST use freshly read state and a timestamp greater than the previous snapshot timestamp; it MUST NOT give stale captured tags a newer timestamp.
Deletion and metadata publication MUST serialize so that a deleted channel cannot regain a metadata snapshot. Repair MUST compare label content, not merely snapshot existence, and MUST NOT republish missing or deleted channels.
A successful command proves its application at a point in time. A later authorized command may supersede its effects.
Acknowledgements, retries and recovery
An exact retry resubmits the same signed event, without changing its tags, timestamp, ID or signature. This NIP introduces no separate receipt event kind or HTTP API.
Retry admission
Version 1 does not provide historical receipt recovery that bypasses freshness or current access checks. Every submission, including an exact retry, MUST pass signature, authenticated-author, transport, community, token-scope and the existing ±900-second timestamp checks before a positive acknowledgement. A positive retry acknowledgement also requires the channel to exist, be unarchived and undeleted, and the author to currently satisfy the label-mutation authority rules above. This applies to retries of creation commands as well as mutations. Channel authority and lifecycle checks MUST share the command's serialization boundary.
After those checks, a retained committed command MUST succeed without reapplication or republication, even if a later command has superseded its effects. The relay MUST perform duplicate detection before a new creation's UUID-conflict check and repeat it within the serialization boundary before any new application.
Failure of current admission MUST reject this submission without applying anything. It MUST NOT turn an earlier committed command into an unapplied command. Opposing edits, archive, channel deletion, channel-role revocation and soft deletion of the command event MUST NOT make the command eligible for reapplication. An expired command MUST NOT be newly applied.
Responses and client outcomes
Once a parseable EVENT covered by this NIP reaches command-specific handling, the relay MUST respond with a NIP-01 OK using the outcomes below. This includes validation and retry-admission failures in that handling. A positive OK for a covered command MUST always mean committed application; generic event storage or deduplication MUST NOT acknowledge it as successful without application evidence.
Earlier generic admission or transport failures, such as frame rejection, authentication admission or handler saturation, MAY instead use ordinary NIP-01 rejection responses or close the connection. They are not NIP-CL receipts. Clients MUST treat a negative response without a recognized nip-cl-rejected or nip-cl-unknown prefix, a disconnect or a missing response as Unknown unless committed application is already known, even if this is the first attempt. The relay MUST NOT use nip-cl-rejected unless it can prove this submission did not apply.
Prefixes in this table are fixed, case-sensitive strings; human-readable details MAY follow them after a space. Clients MUST NOT depend on those details.
| Relay result | OK accepted |
Message or required prefix | Meaning |
|---|---|---|---|
| New application committed, including a no-op | true |
Empty string | This exact command committed. |
| Retained committed application, after retry admission | true |
duplicate: nip-cl-committed |
This exact command committed earlier; nothing was reapplied or republished. |
| Submission rejected before application | false |
invalid: nip-cl-rejected, auth-required: nip-cl-rejected, restricted: nip-cl-rejected, duplicate: nip-cl-rejected, rate-limited: nip-cl-rejected or error: nip-cl-rejected, according to cause |
This submission did not apply. It says nothing about an earlier uncertain attempt. |
| Application outcome cannot be established | false |
error: nip-cl-unknown |
The relay cannot prove commit or rejection. The original outcome remains unknown. |
Malformed commands and timestamp admission failures use invalid:, authentication failures use auth-required:, permission or lifecycle denials use restricted:, and a different creation event targeting a used UUID uses duplicate:. A failure known to have rolled back MAY use error: nip-cl-rejected; an uncertain commit MUST use error: nip-cl-unknown. A stored command without proof of application MUST produce the unknown outcome if admission permits examining it; it MUST NOT be applied again or inferred successful from current labels. Errors MUST NOT reveal inaccessible channel metadata or evidence from another community.
Examples (event IDs abbreviated):
["OK", "<event-id>", true, ""]
["OK", "<event-id>", true, "duplicate: nip-cl-committed"]
["OK", "<event-id>", false, "invalid: nip-cl-rejected timestamp outside admission window"]
["OK", "<event-id>", false, "error: nip-cl-unknown application evidence unavailable"]
Clients MUST persist and reuse the same signed event while delivery is uncertain. They MUST distinguish these states:
| Observation | Client outcome |
|---|---|
Positive OK for the exact event ID from the target relay |
Committed, even if an earlier attempt was uncertain. |
Explicit nip-cl-rejected response, with no earlier uncertain or concurrent attempt for that ID |
Rejected new command. |
nip-cl-unknown, generic rejection without a recognized NIP-CL prefix, timeout, disconnect or an unrecognized response |
Unknown unless committed application is already known. |
| Later rejection, expiry, missing evidence or failed/inaccessible read after an uncertain attempt | Unknown; none proves that the earlier attempt did not commit. |
A later negative response MUST NOT downgrade known committed application. Clients MUST NOT automatically generate a new event ID to resolve an uncertain outcome. Current-label readback is an independent, ACL-filtered observation of current state, not receipt evidence.
Retention
Application evidence MUST survive soft deletion of the command event and MUST be retained until the signed command can no longer pass new-application admission. Physical retention or community deletion MAY remove evidence after that point. Community deletion MUST fence subsequent writes. Without retained evidence, an expired command MUST NOT apply and an uncertain original outcome remains unknown. Indefinite receipt recovery is not promised.
Compatibility and security considerations
Clients that do not use labels can continue ordinary channel operations. Such operations MUST preserve existing labels. Clients MAY ignore label presentation without affecting navigation or channel identity.
Relays MUST enforce the canonical value, count, size, command-shape and authorization rules before committing an operation. A label prefix is not a permission, ownership claim or trusted application identity.
Implementations MUST keep every metadata-writing path label-preserving while labels exist. Disabling new label writes is not sufficient to make an incompatible metadata writer safe. Competing draft snapshot formats (label or additional t tags) are not aliases: implementations using them MUST migrate state and republish canonical snapshots before advertising nip-cl, without guessing whether an arbitrary t value was a label. This specification does not define those migration mechanics.
Labels are shared metadata and are not suitable for secrets or private per-user notes. Exact retries remain subject to current admission; they do not restore access. Current channel metadata remains protected by its current ACL.