decentrl.
Specifications

DCTRL-0007: Public Channels

One-way public broadcasting — signed plaintext events readable by anyone without a communication contract.

FieldValue
RFCDCTRL-0007
TitlePublic Channels
StatusDraft
Created2026-03-14
Version0.1
RequiresDCTRL-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

  1. Introduction
  2. Terminology
  3. Overview
  4. ONE_WAY_PUBLIC Channel
  5. Publishing Public Events
  6. Reading Public Events
  7. Deleting Public Events
  8. Event-Level Signatures
  9. Discovery
  10. Rate Limiting
  11. Security Considerations
  12. 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:

  1. A new ONE_WAY_PUBLIC command channel for authenticated publishing.
  2. A new unauthenticated HTTP endpoint for public reading.
  3. 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 new ONE_WAY_PUBLIC channel type. The publisher sends a signed command to their own mediator.
  • Reading uses a new GET /public/:did endpoint. 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:

  1. Validate replay protection -- timestamp window and nonce deduplication [DCTRL-0004 Section 5].
  2. Resolve the sender's DID to a DID document. Error: SENDER_NOT_FOUND
  3. Extract the signing public key from the verification method matching sender_signing_key_id. Error: SENDER_SIGNING_KEY_NOT_FOUND
  4. Verify the command signature over canonical JSON of { header, payload }. Error: INVALID_SIGNATURE
  5. Verify the sender is registered with this mediator (has an active mediator contract). Error: UNAUTHORIZED_COMMAND
  6. Verify recipient_did equals 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>"
}
FieldTypeRequiredDescription
typestringREQUIRED"PUBLISH_PUBLIC_EVENT"
channel_idstringREQUIREDPublisher-defined channel name (1-64 characters). Scoped to the publisher's DID.
eventstringREQUIREDThe plaintext event as a JSON string. NOT encrypted.
tagsstring[]OPTIONALPlaintext tags for filtering and categorization. Maximum 20 tags per event, each up to 128 characters.
timestampnumberREQUIREDUnix timestamp in seconds when the event was created.
event_signaturestringREQUIREDEd25519 signature over { channel_id, event, tags, timestamp } [Section 8].

Behavior:

  1. Validate the payload against the schema.
  2. Verify the event_signature against the sender's signing public key [Section 8].
  3. Verify channel_id is between 1 and 64 characters.
  4. Verify tags contains at most 20 entries, each at most 128 characters.
  5. Verify event does not exceed the maximum payload size (RECOMMENDED: 64 KB).
  6. Store the public event and its tags in the PublicEvent table.

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:

FieldTypeDescription
idstringServer-generated unique identifier
owner_didstringPublisher's DID
channel_idstringPublisher-defined channel name
eventstringPlaintext JSON string
tagsstring[]Plaintext tags for filtering
timestampnumberEvent creation timestamp (unix seconds)
event_signaturestringEd25519 event-level signature
created_atdatetimeServer-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/:did

This endpoint requires NO authentication. It is publicly accessible.

6.2 Query Parameters

ParameterRequiredTypeDescription
channel_idNostringFilter by channel. If omitted, returns events from all channels.
tagsNostringComma-separated list of tags. Returns events matching at least one tag (OR).
afterNonumberUnix timestamp in seconds. Return events created after this time.
beforeNonumberUnix timestamp in seconds. Return events created before this time.
pageNonumberZero-based page index. Default: 0.
page_sizeNonumberRecords 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:

  1. Resolve the publisher's DID to a DID document.
  2. Extract the signing public key.
  3. Verify the event_signature over { 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:

  1. Verify the event exists and is owned by the command sender.
  2. Hard-delete the event from the PublicEvent table.

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:

  1. Command signature -- authenticates the publishing act (standard command envelope signature [DCTRL-0004 Section 4.4]). Consumed by the mediator.
  2. 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:

  1. Obtain the publisher's DID (shared out-of-band, linked on a website, discovered via another event, etc.).
  2. Resolve the DID to a DID document [DCTRL-0001].
  3. Extract the mediator service endpoint from the DID document's service array.
  4. 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:decentrl DID Method".
  • [DCTRL-0002] "Cryptographic Operations".
  • [DCTRL-0004] "Mediator Protocol".

12.2 Informative References

  • [DCTRL-0003] "Communication Contracts".
  • [DCTRL-0005] "Event Encryption and Storage".