decentrl/docs
Communication

Public Channels

One-way public broadcasting — signed, unencrypted events that anyone can read without a contract.

Formal spec: DCTRL-0007 — Public Channels defines the ONE_WAY_PUBLIC channel type, event format, signature scheme, and query protocol.

Public channels are the counterpart to Decentrl's private communication. While contracts establish encrypted, bilateral channels between two identities, public channels let any identity broadcast signed events that anyone can read — no contract, no encryption, no mutual consent required.

When to Use Public Channels

Private events (via DIRECT_AUTHENTICATED and TWO_WAY_PRIVATE channels) are ideal when data should only be visible to the sender and a specific recipient. Public channels are for data that should be openly accessible:

  • Social feeds — posts, articles, status updates
  • Public profiles — bio, avatar, display name
  • Announcements — release notes, policy changes
  • Discovery — published service capabilities, public metadata

How It Works

The flow is simpler than private communication because there's no encryption or contract negotiation:

  1. Publisher signs the event — { event, tags, timestamp, event_id } is signed with Ed25519
  2. Publisher sends a ONE_WAY_PUBLIC command to their mediator
  3. Mediator verifies the command signature AND the event signature, then stores the event — unless that event_id is already published, in which case it returns the existing post
  4. Anyone can query via a simple GET /public/{did} endpoint — no authentication needed
  5. Readers verify the event signature against the publisher's public key (embedded in their DID). The SDK does this on every read — queryPublicEvents, readPublicState and channel subscriptions — and also checks the event is a declared type whose data passes its schema

What's Signed

Every public event carries a signature over:

{
  event: string        // JSON-serialized event payload
  tags: string[]       // publisher-asserted metadata
  timestamp: number    // Unix seconds
  event_id: string     // publisher-chosen ID, unique per publisher
}

This means readers can verify:

  • The event content wasn't tampered with
  • The tags are authentic (not injected by the mediator)
  • The timestamp was set by the publisher
  • The event ID is the publisher's, so one post can't be served as several or relabeled
  • The event was published by the DID that claims it

The mediator is a relay — it stores and serves events but cannot forge them.

Posts published before event IDs were signed without one. They no longer verify and readers skip them. The SDK also drops every post of a publisher whose DID yields no signing key: nothing is accepted unverified. In the SDK, meta.timestamp is the signed timestamp converted to unix ms.

Retries and Event IDs

The event_id makes publishing safe to retry. The mediator keeps one post per (publisher, event_id): publishing the same ID again stores nothing and returns the first post's public_event_id with duplicate: true. The content isn't compared, so a retry that was re-signed with a new timestamp still resolves to the original post. To change a post, publish a new one and delete the old.

There are two IDs per post: the signed event_id (what readers dedupe on) and the mediator's public_event_id (what deletion takes). GET /public/{did} returns both, as event_id and id.

Tags

Public events can be organized by tags — metadata labels for filtering. Tags are plaintext (unlike private event tags which are encrypted). Tag templates support interpolation: post:${title}.

Readers can filter by tags when querying.

Public vs Private — Comparison

Private EventsPublic Events
EncryptionAES-256-GCM with the contract's daily epoch keyNone (plaintext)
Contract requiredYes — bilateral agreementNo
VisibilitySender + recipient onlyAnyone
TagsEncrypted (HMAC-based)Plaintext
SignatureCommand signature + signed envelope (inside the encryption)Command signature + event signature
Event IDSigned inside the envelope; never seen by the mediatorSigned and public; unique per publisher
StoragePer-identity encrypted storageShared public storage
Query authRequires signed commandSimple HTTP GET
Channel typeDIRECT_AUTHENTICATED / TWO_WAY_PRIVATEONE_WAY_PUBLIC

Combining Public and Private Events

A typical app uses both. For example, a social app might use:

  • Public channels for posts, profile updates, and follower counts
  • Private channels for direct messages, reactions, and read receipts

Both event types flow through the same defineDecentrlApp configuration and the same state reducers — they just use different prefixes (public: for own public events, channel: for external public events). client.readPublicState(did) runs another identity's verified public events through your public: reducers, so a profile page reuses the reducers that build your own profile.

Security Considerations

  • No confidentiality — public events are stored in plaintext. Don't publish sensitive data.
  • Integrity guaranteed — signatures prevent tampering. Readers should always verify signatures.
  • Publisher authentication — the signing key is embedded in the publisher's DID, so anyone can verify authorship.
  • Mediator trust — the mediator can withhold events (censorship) but cannot forge or modify them. Readers who want censorship resistance can query multiple mediators or use content-addressed storage.
  • Tags are public — unlike private events where tags are encrypted, public event tags reveal metadata. Design tag schemas accordingly.