Educational article. Every claim about the platform points back to official Polkadot documentation or the public repos cited. The application section describes an experimental prototype (CypherTux / ctuxhello.dot) and is not a security audit.
Why this article
Many web developers already know a familiar stack: local build, cloud hosting, DNS, OAuth login or a wallet extension. Polkadot Products keeps that mental model (you build a front, you publish it, users open it) but replaces several central bricks.
The goal here is not to sell a vision: it is to explain what the platform offers, how the pieces fit together, and how one concrete Product (a sealed dead-drop) composes those mechanisms : with official links at every step.
Official entry point: Apps | Polkadot Developer Docs.
What is a Polkadot Product?
From the Apps documentation:
Polkadot Products are what you build: third-party applications that run inside one of the Polkadot Apps. Products are sandboxed single-page apps (HTML / JS / CSS), addressed by .dot names […], registered on-chain through a decentralized name service, and they never see the user's private key. The bundle itself is published to a decentralized cloud storage provider and fetched by the Host on demand.
Source: docs.polkadot.com/apps.
In practice:
- You ship a front-end bundle (SPA).
- You publish it via the Host’s Cloud Storage capability, which is backed by Bulletin (content-addressed storage).
- You address it with a .dot name via dotNS.
- The user opens it in a Host (sandbox), the private key stays outside the Product.
You do not deploy three native apps: the same Product can be used across the three Triangle Hosts (Mobile App, Desktop, Web), with different capabilities depending on the Host.
The Triangle: where the app runs, where the key lives
The three Hosts form the Polkadot Triangle (glossary):
| Host | Documented role |
|---|---|
| Polkadot App (mobile) | Holds the private key, approves every signature |
| Polkadot Desktop | Loads .dot Products in a sandbox, does not hold the key |
| Polkadot Web (dot.li) | Browser Host, sandboxed iframe, signing via a paired App |
References:
Structural point (repeated in the App docs): Desktop and Web never hold the key. Pairing creates a session (session public key), every signature goes back to the phone.
For a web developer, the useful analogy is not “MetaMask in the browser”, it is rather: the Product runtime is the Host, the private-key vault remains the phone.
Three identities, deliberately separated
The Identity page distinguishes three notions:
- The .dot name : names the Product as an addressable resource (routing to the content CID), not a user. On-chain ownership (dotNS / Asset Hub), but the name does not sign.
- The per-app account (product account) : derived for this user in this Product. Same person on app-a.dot and app-b.dot → different addresses by default (anti-correlation).
- Proof of Personhood : tier (None / Lite / Full) + per-app alias (Ring-VRF), for “unique human” without exposing who.
So the .dot names the Product for addressing, that same name usually serves as productId in product-account derivation. Derivation (Identity docs): junctions product / productId / derivationIndex in soft sr25519. Address forms: SS58 and H160 (pallet-revive contracts on Asset Hub).
SDK: SignerManager.getProductAccount : see also Sign and Submit Transactions and Account Management (TrUAPI).
Where to put data: four placement mechanisms
Official decision guide: Where to Store Data.
| Mechanism | Good use (docs) | Persistence | Size |
|---|---|---|---|
| Contract storage | Enforceable state / rules | Permanent | Small, structured |
| Cloud storage (via Bulletin) | Files, blobs, posts | Limited, renewable | Large |
| Statement Store | Real-time signal | Seconds (ephemeral) | Up to 512 bytes |
| Local storage | Preferences, device cache | Until cleared | Small |
Recommended pattern on the same page: Statement Store announces a change, Bulletin carries the content (CID), optionally a contract references the CID, local storage speeds up the UI.
Classic documented mistake: putting bulk data in a contract. The docs explicitly say to store bytes on Bulletin and keep only a CID on-contract if needed.
Bulletin Chain: files = bytes + CID
Role
The Bulletin Chain is the content-addressed storage infrastructure for Products. On the Product/Host side one often talks about the Cloud Storage capability, Bulletin is the underlying substrate (“decentralized cloud storage” in the docs).
References:
- Bulletin Chain Reference
- Data Storage (Hub)
- Store Data on Chain
- Repo: paritytech/polkadot-bulletin-chain
You submit bytes, the system produces a CID that cryptographically identifies the content. In the model documented here, the underlying fingerprint is typically Blake2b-256 and the CID is compatible with IPFS mechanisms. With identical content and encoding, the CID stays the same. To read the content, the client uses that CID to retrieve it from the network.
What is on-chain vs served
The Bulletin (Products) docs describe a split (schema still marked provisional on some details):
- On-chain: records (CID, authorizations, expiration).
- Collator / P2P network: delivery of the bytes (Bitswap / documented IPFS paths).
In other words: it is not “every file lives forever in the state trie like a balance”. It is a storage-specialized parachain, with on-chain commitment and gating, and byte distribution on the collator side.
TestNet endpoints cited in Data Storage: RPC wss://paseo-bulletin-rpc.polkadot.io, gateway https://paseo-ipfs.polkadot.io : with a warning: generic IPFS gateways (ipfs.io, etc.) are not reliable for Bulletin.
Gated writes (authorization)
Bulletin does not use native token balances to pay for storage “the Ethereum way”. Write access goes through an authorization (tx and byte quotas, with expiration). Sources: Data Storage : Authorization, Bulletin Chain Reference.
At the Bulletin level, writes are controlled by authorizations, on the Host/Product side, those rights are exposed and consumed via capability allowances. On TestNet, the docs point to a faucet / Console UI to obtain an authorization. Inside a Host, allocation often goes through a Bulletin allowance (the Product asks, the Host/App allocates based on availability).
Retention and renew
Docs: Bulletin Chain Renewal.
- Default window: about two weeks.
- renew: extends expiration of the same record, the CID does not change, it is not a re-upload.
- Documented cost: consumes one transaction from the authorization quota, bytes are not recounted as a new upload.
- Miss a renew → no availability guarantee, after collator eviction, you must re-upload the same bytes (same CID possible, new tx).
The docs still mark some exact parameters as provisional (grace period, batch renew). The conceptual cycle (TTL + renew) is presented as stable.
On the Bulletin runtime design / README side, the soft-cap (store) / hard-cap (renew) model and a global “permanent” (renewed) storage ceiling are described to avoid unbounded growth from widespread renew : see paritytech/polkadot-bulletin-chain.
Sizes (TestNet, Hub docs)
From Data Storage : Size Limits:
- ~8 MiB max per transaction
- ~64 MiB max chunked file (DAG-PB / UnixFS)
- ~2 weeks retention
Large bundles (including a deployed front) are chunked, the root CID (manifest) is what you publish / reference.
dotNS: a .dot name points to a CID, not an IP
References:
Points checked in the docs:
- Role analogous to DNS: human name → content.
- Differences: on-chain, resolution to a CID (Product bundle), not an IP, pricing tied to Proof of Personhood to limit short-name farming.
- Registry on Asset Hub (contracts), not on People or Bulletin.
- Typical Host flow: navigate to myproduct.dot → namehash → read contenthash on Asset Hub → decode CID → fetch bytes (IPFS gateway and/or Bulletin P2P) → verify bytes against the CID → load in the sandbox.
Pedagogical consequence: republishing an app often means publishing new bytes (new CID) then updating the .dot contenthash. Bulletin renew, by contrast, keeps the same CID.
Statement Store: signal, do not archive
Reference: Statement Store.
Documented characteristics:
- Short, signed messages, gossip / local node pool : not stored in blocks as permanent history.
- Short TTL, data-placement: up to 512 bytes, expiration in seconds.
- Best-effort delivery (no guaranteed retry/ACK/order at the protocol level).
- Documented composition: if content must survive, Bulletin for the bytes, Statement Store to announce a CID / live signal (e.g. the App’s Chat pattern).
It is the conceptual equivalent of ephemeral pub/sub, not a database.
SDK and Host constraint
Packages and recipes: Build, product-sdk (GitHub).
Documented package examples:
- @parity/product-sdk-signer : accounts + signing
- @parity/product-sdk-host : Host capabilities (including Preimage / allowances)
- @parity/product-sdk-cloud-storage : Bulletin / Cloud Storage client
- @parity/product-sdk-statement-store : pub/sub
Products engineering rule: the Product runs inside the Host and goes through the Host API for accounts, signing, storage. The cloud-storage docs also stress that some CID reads are container-only (no public gateway fallback in the SDK) : see the cloud-storage skill / package and Store Data on Chain.
Local dev: Desktop can load localhost without .dot resolution (Build : Set Up).
Deploy: bundle → Bulletin + name registration via CLI / playground tools (see Apps Quick Start / Deploy docs, playground CLI: paritytech/playground-cli).
Case study: CypherTux (ctuxhello.dot)
Status
Educational prototype / PoC, unaudited, derived from the Playground template. Observed stack in the repo: React 19 + Vite + TypeScript, @parity/product-sdk-* packages (signer, host, cloud-storage, statement-store).
Working deployment mentioned on Products Devnet: name ctuxhello.dot (gateway of the xxx.dev-dot.li kind depending on environment). The cyphertux.dot branding target is not treated here as a verified on-chain ownership fact.
Product intent
A sealed dead-drop:
- The user enters a message and a passphrase.
- The client encrypts locally (Web Crypto).
- Only the sealed envelope (JSON of bytes) is published on Bulletin.
- A board (public index of claims: CID + non-secret metadata) lists the drops.
- To open: CID (or link) + the same passphrase.
Confidentiality does not rest on “the server hides the text”: Bulletin reads are designed as open to whoever has the CID (Bulletin Reference: permissionless reads, privacy = encrypt before store). That is exactly the model the app applies.
Client-side crypto (what the code does)
File src/cryptoDrop.ts (factual summary from the repo):
- PBKDF2-SHA-256 (120,000 iterations) from passphrase + salt
- AES-256-GCM
- UX soft cap: 500 plaintext characters
- Versioned envelope (fields v, s, n, c: salt / IV / ciphertext+tag in base64)
The code’s own warning: experimental, unaudited. Metadata (CID, size, upload signer, public display name) remains visible.
How the Product mechanisms compose (aligned with the docs)
| Need | Mechanism used in CypherTux | Docs alignment |
|---|---|---|
| Sealed message available for the Bulletin retention window | Bulletin via Host getPreimageManager().submit | Store Data on Chain, data-placement |
| Board (public index of claims) | JSON index also on Bulletin (index CID) | Same “bytes on Bulletin” logic |
| Announce the board head | Statement Store ChannelStore, board-head channel | “SS announces, Bulletin carries” pattern (Statement Store, data-placement) |
| Local cache / labels | localStorage | Local storage |
| Account / login | Host + SignerManager / product account | Identity, Signer |
Important implementation detail (repo): Bulletin upload goes through Host PreimageManager (Bulletin allowance), not through a CloudStorageClient.store wired to another signing path : explicit comment in bulletinDrop.ts about observed Desktop→Mobile failures with CreateTransaction. That is a real Host integration constraint, not a cosmetic detail.
Claim links and resilience
The UI builds claim links of the form ?cid=…&board=…:
- cid: sealed envelope
- board: index CID (optional) to sync the board without relying only on Statement Store gossip
When Statement Store is not “Live” (missing allowance / degraded sync), the Product can still work by sharing the claim link : consistent with the documented fact that Statement Store is best-effort and allowance-gated.
What the app is not
- Not an end-to-end encrypted messenger like Signal (no Host user-key model for decrypt in this PoC).
- Not proof that “Bulletin = forever”: official retention remains ~2 weeks + renew.
- Not a GitHub replacement: source code stays off-chain, only publication of the Products bundle / blobs uses Bulletin + dotNS.
Classic web parallel (without romance)
- Front-end build → SPA in the Host sandbox
- S3 / asset storage → Bulletin (bytes + CID)
- DNS / IP → dotNS / CID
- Wallet extension / OAuth → Host + mobile App (key)
- Redis / presence → Statement Store
- localStorage → Host/device local storage
What is arguable from the docs, without inventing:
- One bundle, three Hosts (Triangle).
- Verifiable content by hash (CID), no blind trust in “the author’s server”.
- Per-Product account to limit cross-app correlation.
- Key outside the Product, signing on Mobile.
- Explicit data placement (contract vs Bulletin vs SS vs local) instead of a single “backend”.
Limits equally arguable from the docs:
- Bounded Bulletin retention + renew / quotas.
- Ephemeral, best-effort Statement Store.
- Gated writes (authorizations / allowances).
- Sensitive reads: encrypt on the Product side.
- Platform still moving (provisional mentions on Bulletin schemas, exact renew parameters, etc.).
Going further (reading path)
Suggested order: