DCTRL-0007Public Channels
One-way public broadcasting — signed plaintext events readable by anyone without a communication contract.
| Field | Value |
|---|---|
| RFC | DCTRL-0007 |
| Title | Public Channels |
| Status | Draft |
| Created | 2026-03-14 |
| Version | 0.1 |
| Requires | DCTRL-0001, DCTRL-0002, DCTRL-0004 |
Abstract
This document specifies public channels for the Decentrl Protocol. A public channel allows an identity to publish signed, unencrypted events that any party can read without a communication contract. Events are signed by the publisher for authenticity verification. This enables use cases such as public announcements, social media posts, blogs, public profiles, and status broadcasts.
Status of This Document
This is a draft specification.
Table of Contents
- Introduction
- Terminology
- Overview
- ONE_WAY_PUBLIC Channel
- Publishing Public Events
- Reading Public Events
- Deleting Public Events
- Event-Level Signatures
- Discovery
- Rate Limiting
- Security Considerations
- References
1. Introduction
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119].
The existing Decentrl communication channels (DIRECT_AUTHENTICATED and TWO_WAY_PRIVATE) require both parties to establish a communication contract before exchanging events. All events are encrypted end-to-end.
Public channels introduce a fundamentally different communication model: one-to-many, unencrypted, no contract required. The publisher signs each event so readers can verify authenticity, but the content is plaintext and accessible to anyone.
This specification adds:
- A new
ONE_WAY_PUBLICcommand channel for authenticated publishing. - A new unauthenticated HTTP endpoint for public reading.
- An event-level signature scheme for transport-independent verification.
2. Terminology
Publisher -- An identity registered with a mediator that publishes public events.
Public Event -- A signed, unencrypted event published by an identity.
Event ID -- The publisher-chosen, signed identifier of a public event, unique per publisher. It uses the same format as private event IDs [DCTRL-0005 Section 3.2]: 1 to 128 characters from A-Z a-z 0-9 . _ : -. A retry reuses it.
Public Event ID -- The mediator's server-generated identifier of a stored public event (public_event_id, or id when reading). It is what DELETE_PUBLIC_EVENT takes.
Reader -- Any party that queries a publisher's public events. Readers do not need to be registered with any mediator or hold any keys.
3. Overview
Public channels separate writing (authenticated) from reading (open):
- Publishing uses the existing
POST /command infrastructure with a newONE_WAY_PUBLICchannel type. The publisher sends a signed command to their own mediator. - Reading uses a new
GET /public/:didendpoint. No authentication required. Anyone who knows the publisher's DID can query their public events.
Public events are stored in plaintext on the publisher's mediator. There is no encryption, no recipient, and no communication contract involved. The mediator stores and serves the events as-is.
4. ONE_WAY_PUBLIC Channel
4.1 Purpose
The ONE_WAY_PUBLIC channel is used for publishing signed plaintext events. The publisher authenticates with their Ed25519 signing key. The mediator verifies the signature and stores the event publicly.
4.2 Authentication Flow
For commands with channel: "ONE_WAY_PUBLIC", the mediator MUST:
- Apply the common command checks in the order of [DCTRL-0004 Section 5.4]:
- Protocol version. Error:
UNSUPPORTED_VERSION - Timestamp window. Error:
TIMESTAMP_OUT_OF_RANGE - Resolve the sender's DID to a DID document. Error:
SENDER_NOT_FOUND - Extract the signing public key from the verification method matching
sender_signing_key_id. Error:SENDER_SIGNING_KEY_NOT_FOUND - Verify the command signature over canonical JSON of
{ header, payload }. Error:INVALID_SIGNATURE - Record the nonce. Error:
DUPLICATE_NONCE - Per-sender command rate limit. Error:
RATE_LIMIT_EXCEEDED
- Protocol version. Error:
- Verify the sender is registered with this mediator (has an active mediator contract). Error:
UNAUTHORIZED_COMMAND - Verify
recipient_didequals the mediator's own DID. Error:UNAUTHORIZED_COMMAND
4.3 Command Envelope
{
"header": {
"version": 1,
"channel": "ONE_WAY_PUBLIC",
"sender_did": "<publisher DID>",
"sender_signing_key_id": "<publisher DID>#signing",
"recipient_did": "<mediator DID>",
"timestamp": 1699123456000,
"nonce": "<UUID v4>"
},
"payload": { ... },
"signature": "<base64 Ed25519 signature>"
}The recipient_did is the mediator's DID. The mediator is the custodian of the public channel, not the audience.
5. Publishing Public Events
5.1 PUBLISH_PUBLIC_EVENT
Payload:
{
"type": "PUBLISH_PUBLIC_EVENT",
"event": "<JSON-stringified plaintext event>",
"tags": ["topic.decentrl", "type.announcement"],
"timestamp": 1699123456,
"event_id": "1b4e28ba-2fa1-11d2-883f-0016d3cca427",
"event_signature": "<base64 Ed25519 signature>"
}| Field | Type | Required | Description |
|---|---|---|---|
type | string | REQUIRED | "PUBLISH_PUBLIC_EVENT" |
event | string | REQUIRED | The plaintext event as a JSON string. NOT encrypted. |
tags | string[] | OPTIONAL | Plaintext tags for filtering and categorization. Maximum 20 tags per event, each up to 128 characters. |
timestamp | number | REQUIRED | Unix timestamp in seconds when the event was created. |
event_id | string | REQUIRED | The event ID [Section 2]. An invalid one fails schema validation (INVALID_COMMAND). |
event_signature | string | REQUIRED | Ed25519 signature over { event, tags, timestamp, event_id } [Section 8]. |
Behavior:
- Validate the payload against the schema.
- Verify the
event_signatureagainst the sender's signing public key [Section 8]. - Verify
tagscontains at most 20 entries, each at most 128 characters. - Verify
eventdoes not exceed the maximum payload size (RECOMMENDED: 64 KB). - Store the public event and its tags in the
PublicEventtable, unless the publisher already has a public event with thisevent_id(step 7). - If it does, store nothing and return the existing event's
public_event_idwithduplicate: true.
Response:
{
"type": "SUCCESS",
"public_event_id": "<server-generated ID>",
"duplicate": false
}duplicate is true when the event_id was already published and the existing post is returned.
First write wins. The mediator MUST keep at most one public event per (owner_did, event_id), enforced by a unique constraint so concurrent publishes of one event_id store one post. A repeat is not compared with the stored post: a retry is re-signed with a new timestamp, so its content legitimately differs, and it still resolves to the first post. A publisher that wants to change a post publishes a new event (with a new event_id) and deletes the old one.
5.2 Storage Model
The mediator MUST store public events with at least the following fields:
| Field | Type | Description |
|---|---|---|
id | string | Server-generated unique identifier |
owner_did | string | Publisher's DID |
event | string | Plaintext JSON string |
tags | string[] | Plaintext tags for filtering |
timestamp | number | Event creation timestamp (unix seconds) |
event_id | string | The publisher's signed event ID; unique per owner_did |
event_signature | string | Ed25519 event-level signature |
created_at | datetime | Server-side creation time |
The mediator MUST have a unique index on (owner_did, event_id) [Section 5.1] and SHOULD index on (owner_did, timestamp) for efficient querying. Posts stored before event IDs have a null event_id. Tags SHOULD be stored in a separate relation table indexed on (owner_did, tag) for efficient tag-based lookups.
5.3 Tags
Tags are plaintext strings that allow readers to filter public events. Unlike private events [DCTRL-0005] where tags are encrypted because the mediator is encryption-blind, public event tags are stored and queried in plaintext since the event content itself is public.
Tags follow the same conceptual model as encrypted tags — they are application-defined labels computed from event data. The SDK's tag template system (e.g., thread.${threadId}) works identically, but the output is stored as-is rather than encrypted.
When multiple tags are provided in a query, the mediator MUST return events that match at least one of the requested tags (OR semantics).
6. Reading Public Events
6.1 HTTP Endpoint
A conforming mediator MUST implement the following endpoint:
GET /public/:didThis endpoint requires NO authentication. It is publicly accessible.
6.2 Query Parameters
| Parameter | Required | Type | Description |
|---|---|---|---|
tags | No | string | Comma-separated list of tags. Returns events matching at least one tag (OR). |
after | No | number | Unix timestamp in seconds. Return events created after this time. |
before | No | number | Unix timestamp in seconds. Return events created before this time. |
page | No | number | Zero-based page index. Default: 0. |
page_size | No | number | Records per page. Default: 20. Maximum: 100. |
The :did path parameter is the publisher's DID, URL-encoded.
6.3 Response
200 OK:
{
"publisher_did": "<DID>",
"events": [
{
"id": "<server-generated ID>",
"event_id": "1b4e28ba-2fa1-11d2-883f-0016d3cca427",
"event": "<JSON string (plaintext)>",
"tags": ["topic.decentrl", "type.announcement"],
"timestamp": 1699123456,
"event_signature": "<base64 Ed25519 signature>"
}
],
"pagination": {
"page": 0,
"page_size": 20,
"total": 42
}
}Events MUST be ordered by timestamp descending (newest first). event_id is null for posts stored before event IDs.
404 Not Found:
If the :did is not registered with this mediator:
{
"type": "ERROR",
"code": "PUBLISHER_NOT_FOUND"
}6.4 Signature Verification by Readers
Readers MAY verify each event's authenticity:
- Resolve the publisher's DID to a DID document.
- Extract the signing public key.
- Verify the
event_signatureover{ event, tags, timestamp, event_id }[Section 8].
If verification succeeds, the reader can trust that the event was authored by the DID holder. A malicious mediator cannot forge events because it does not hold the publisher's signing private key.
A reader that verifies SHOULD skip events that fail, and SHOULD dedupe on (publisher DID, event_id) rather than on the server-generated id. Posts without an event_id, stored before event IDs, cannot be verified against the current signed data and are skipped; they are not migrated.
7. Deleting Public Events
7.1 DELETE_PUBLIC_EVENT
The publisher MAY delete their own public events.
Payload:
{
"type": "DELETE_PUBLIC_EVENT",
"public_event_id": "<server-generated ID>"
}Behavior:
- Verify the event exists and is owned by the command sender.
- Hard-delete the event from the
PublicEventtable.
Response:
{ "type": "SUCCESS" }If the event does not exist or is not owned by the sender:
{
"type": "ERROR",
"code": "PUBLIC_EVENT_NOT_FOUND"
}7.2 Non-Repudiation
Deletion removes the event from the mediator's storage. However, readers who previously fetched the event retain their copy and the event-level signature. The protocol does not provide a mechanism to recall events from readers. Publishers SHOULD treat public events as potentially permanent once published.
8. Event-Level Signatures
8.1 Purpose
Public events carry two signatures:
- Command signature -- authenticates the publishing act (standard command envelope signature [DCTRL-0004 Section 4.4]). Consumed by the mediator.
- Event signature -- authenticates the event content. Survives independently of the transport, enabling third-party verification.
8.2 Signing
The event signature is computed over canonical JSON of { event, tags, timestamp, event_id }:
signable = {
event: payload.event,
tags: payload.tags ?? [],
timestamp: payload.timestamp,
event_id: payload.event_id
}
event_signature = sign_json_object(signable, publisher_signing_private_key)Tags are included in the signature to prevent a malicious mediator from adding, removing, or modifying tags. If no tags are provided, an empty array is used for signature computation.
The event ID is signed because readers dedupe on it. Unsigned, a mediator could serve one post several times under different IDs, and a reader would apply it more than once; or it could strip or swap an ID, and a reader would drop a post as a duplicate of another. Signed, one post has exactly one verifiable ID. Versions of this specification without event_id signed { event, tags, timestamp }; such signatures no longer verify.
This uses the same canonical JSON serialization and Ed25519 signing as defined in [DCTRL-0002].
8.3 Verification
To verify an event signature:
signable = {
event: event.event,
tags: event.tags ?? [],
timestamp: event.timestamp,
event_id: event.event_id
}
valid = verify_json_signature(signable, event.event_signature, publisher_signing_public_key)The publisher's signing public key is obtained by resolving their DID document [DCTRL-0001].
9. Discovery
9.1 Finding Public Channels
Readers discover public channels through the publisher's DID:
- Obtain the publisher's DID (shared out-of-band, linked on a website, discovered via another event, etc.).
- Resolve the DID to a DID document [DCTRL-0001].
- Extract the mediator service endpoint from the DID document's
servicearray. - Query
GET /public/<publisher-did>on the mediator endpoint.
9.2 No DID Document Changes
This specification does not require changes to the DID document format. The mediator service endpoint [DCTRL-0001] is sufficient for discovering public channels.
The convention is that any mediator endpoint that hosts a registered identity MAY serve public events at /public/:did if the identity has published any.
10. Rate Limiting
10.1 Publishing Limits
Mediators MUST rate-limit public event publishing per publisher DID. The default is 100 PUBLISH_PUBLIC_EVENT commands per hour per DID (fixed window, configurable via RATE_LIMIT_PUBLIC_EVENTS_PER_HOUR). This limit applies in addition to the general per-sender command limit [DCTRL-0004 Section 5.5], and only authenticated commands are counted.
When a publisher exceeds the rate limit, the mediator MUST respond with (HTTP 429):
{
"type": "ERROR",
"code": "RATE_LIMIT_EXCEEDED"
}10.2 Payload Size Limits
The mediator SHOULD enforce a maximum payload size for the event field. The RECOMMENDED limit is 64 KB.
When the payload exceeds the size limit:
{
"type": "ERROR",
"code": "PAYLOAD_TOO_LARGE"
}10.3 Reading Limits
Mediator operators MAY apply rate limiting to the GET /public/:did endpoint to prevent abuse. This is a deployment concern and not specified by the protocol.
11. Security Considerations
11.1 No Encryption
Public events are stored and served in plaintext. Publishers MUST NOT include sensitive information in public events. The protocol provides no confidentiality for public channel content.
11.2 Authenticity
Event-level signatures [Section 8] provide authenticity guarantees:
- Readers can verify that an event was authored by the claimed publisher.
- A compromised mediator cannot forge events (it lacks the publisher's signing key).
- Event signatures are self-contained -- they can be verified by any party with the publisher's public key.
11.3 Integrity
The event signature covers { event, tags, timestamp, event_id }. A mediator cannot modify any of these fields without invalidating the signature, so it cannot present one post as several or relabel a post's ID. However, a malicious mediator can:
- Omit events -- selectively hide events from readers.
- Reorder events -- serve events in a different order (readers SHOULD verify timestamps).
- Serve stale data -- not return the latest events.
These are availability attacks, not integrity attacks. The protocol does not currently address them. Future versions MAY add sequence numbers or hash chains for tamper detection.
11.4 Spam and Abuse
Public events are write-protected by registration and signature requirements -- only the registered identity can publish its own events. Read access is open by design.
Rate limiting [Section 10] provides the primary defense against publishing abuse. Mediators enforce a default limit; operators MAY tune it.
11.5 Privacy
Publishing to a public channel reveals the publisher's DID and the event content to anyone. Publishers SHOULD be aware that public events are, by definition, public.
The mediator can observe all public event content (unlike private events where the mediator is encryption-blind). This is an inherent property of unencrypted public channels.
11.6 Metadata Exposure
The GET /public/:did endpoint confirms whether a DID is registered with a particular mediator. Mediator operators who consider this a privacy concern MAY return 404 for all unregistered DIDs (which is the default behavior).
12. References
12.1 Normative References
- [RFC 2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997.
- [DCTRL-0001] "The
did:decentrlDID Method". - [DCTRL-0002] "Cryptographic Operations".
- [DCTRL-0004] "Mediator Protocol".
12.2 Informative References
- [DCTRL-0003] "Communication Contracts".
- [DCTRL-0005] "Event Encryption and Storage".