DCTRL-0007: Public 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 Channel -- A named stream of public events belonging to a single publisher. Identified by a channel_id string scoped to the publisher's DID.
Public Event -- A signed, unencrypted event published to a public channel.
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 to a public channel. 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:
- Validate replay protection -- timestamp window and nonce deduplication [DCTRL-0004 Section 5].
- 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 - 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": {
"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",
"channel_id": "<string>",
"event": "<JSON-stringified plaintext event>",
"tags": ["topic.decentrl", "type.announcement"],
"timestamp": 1699123456,
"event_signature": "<base64 Ed25519 signature>"
}| Field | Type | Required | Description |
|---|---|---|---|
type | string | REQUIRED | "PUBLISH_PUBLIC_EVENT" |
channel_id | string | REQUIRED | Publisher-defined channel name (1-64 characters). Scoped to the publisher's DID. |
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_signature | string | REQUIRED | Ed25519 signature over { channel_id, event, tags, timestamp } [Section 8]. |
Behavior:
- Validate the payload against the schema.
- Verify the
event_signatureagainst the sender's signing public key [Section 8]. - Verify
channel_idis between 1 and 64 characters. - 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.
Response:
{
"type": "SUCCESS",
"public_event_id": "<server-generated ID>"
}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 |
channel_id | string | Publisher-defined channel name |
event | string | Plaintext JSON string |
tags | string[] | Plaintext tags for filtering |
timestamp | number | Event creation timestamp (unix seconds) |
event_signature | string | Ed25519 event-level signature |
created_at | datetime | Server-side creation time |
The mediator SHOULD index on (owner_did, channel_id, timestamp) for efficient querying. 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 |
|---|---|---|---|
channel_id | No | string | Filter by channel. If omitted, returns events from all channels. |
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>",
"channel_id": "posts",
"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).
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{ channel_id, event, tags, timestamp }[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.
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 { channel_id, event, tags, timestamp }:
signable = {
channel_id: payload.channel_id,
event: payload.event,
tags: payload.tags ?? [],
timestamp: payload.timestamp
}
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.
This uses the same canonical JSON serialization and Ed25519 signing as defined in [DCTRL-0002].
8.3 Verification
To verify an event signature:
signable = {
channel_id: event.channel_id,
event: event.event,
tags: event.tags ?? [],
timestamp: event.timestamp
}
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
Mediator operators SHOULD configure rate limits for public event publishing. The RECOMMENDED default is 100 events per hour per DID.
When a publisher exceeds the rate limit, the mediator MUST respond with:
{
"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 { channel_id, event, tags, timestamp }. A mediator cannot modify any of these fields without invalidating the signature. 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 channels are write-protected by registration and signature requirements -- only the registered identity can publish to its own channels. Read access is open by design.
Rate limiting [Section 10] provides the primary defense against publishing abuse. Mediator operators SHOULD configure appropriate limits.
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".