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:
- Publisher signs the event —
{ event, tags, timestamp, event_id }is signed with Ed25519 - Publisher sends a
ONE_WAY_PUBLICcommand to their mediator - Mediator verifies the command signature AND the event signature, then stores the event — unless that
event_idis already published, in which case it returns the existing post - Anyone can query via a simple
GET /public/{did}endpoint — no authentication needed - Readers verify the event signature against the publisher's public key (embedded in their DID). The SDK does this on every read —
queryPublicEvents,readPublicStateand 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 Events | Public Events | |
|---|---|---|
| Encryption | AES-256-GCM with the contract's daily epoch key | None (plaintext) |
| Contract required | Yes — bilateral agreement | No |
| Visibility | Sender + recipient only | Anyone |
| Tags | Encrypted (HMAC-based) | Plaintext |
| Signature | Command signature + signed envelope (inside the encryption) | Command signature + event signature |
| Event ID | Signed inside the envelope; never seen by the mediator | Signed and public; unique per publisher |
| Storage | Per-identity encrypted storage | Shared public storage |
| Query auth | Requires signed command | Simple HTTP GET |
| Channel type | DIRECT_AUTHENTICATED / TWO_WAY_PRIVATE | ONE_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.