Set Up Your Project

## Set Up Your Project

Every guide assumes a Product project running locally in [Polkadot Desktop](/apps/get-started/). The steps below apply no matter how you bootstrapped your Product: a [Quick Start](/apps/quick-start/) deploy (RevX or CLI) or a project created from scratch.

1. Scaffold a project. Any framework that serves on `localhost` works; this example uses Next.js:

    ```bash
    npx create-next-app@latest my-product
    cd my-product
    ```

2. Install the [Product SDK](https://github.com/paritytech/product-sdk):

    ```bash
    npm install @parity/product-sdk
    ```

3. Start the dev server:

    ```bash
    npm run dev
    ```

4. Open Polkadot Desktop and click **Skip (Dev only)**.

    ![Polkadot Desktop showing the "Skip (Dev only)" button](/images/apps/build/start-a-local-dev-loop/start-a-local-dev-loop-01.webp)

5. Type `localhost:3000` (or your dev server's port) in the address bar and press **Enter**.

    Desktop recognizes `localhost` as a whitelisted origin, skips `.dot` resolution, and loads your Product directly from the local server.

    ![Polkadot Desktop showing a local Product loaded from localhost:3000](/images/apps/build/start-a-local-dev-loop/start-a-local-dev-loop-02.webp)

Your Product is now running inside the Polkadot Desktop sandbox, served from your local machine. Live reload works through your dev server's hot module replacement: edit, save, and watch the change appear in Desktop. The sandbox and permission prompts apply exactly as they would for a published `.dot` Product, and no Proof of Personhood or funds are needed to load from `localhost`.


Capabilities

## Capabilities

-   Beginner **Read On-Chain Data**

    ---

    Your Product needs to show live balances, storage, and chain state. Reads are unsigned: no account, no tokens, no infrastructure to run.

    [:octicons-arrow-right-24: Read On-Chain Data](/apps/build/read-chain-state/)

-   Beginner **Sign and Submit Transactions**

    ---

    Your Product needs to act on chain on the user's behalf. Derive a per-user account and request signatures; every approval happens on the user's phone.

    [:octicons-arrow-right-24: Sign and Submit Transactions](/apps/build/sign-and-submit/)

-   Intermediate **Store Data on Chain**

    ---

    Your Product needs content that outlives a session: profile photos, published posts, file uploads. Write to the [Bulletin Chain](/reference/apps/infrastructure/bulletin-chain/) and fetch from anywhere by CID.

    [:octicons-arrow-right-24: Store Data on Chain](/apps/build/store-data-on-chain/)

-   Intermediate **Publish and Subscribe to Off-Chain Data**

    ---

    Your Product needs real-time state between users: presence, typing indicators, multiplayer cursors. Signed pub/sub via the [Statement Store](/reference/apps/infrastructure/statement-store/), no fees per message.

    [:octicons-arrow-right-24: Publish and Subscribe to Off-Chain Data](/apps/build/pub-sub-off-chain-data/)

-   Beginner **Persist Data Locally**

    ---

    Your Product needs to remember things on this device: preferences, drafts, cached values. Per-Product key-value storage that works inside a Host or in a plain browser tab.

    [:octicons-arrow-right-24: Persist Data Locally](/apps/build/persist-data-locally/)


The product-sdk Packages

## The product-sdk Packages

Each guide is built around one _primary_ package and weaves in _utility_ packages where they are needed. The full source is at [`paritytech/product-sdk`](https://github.com/paritytech/product-sdk). Here is what each package is for:

|      Package      |                                                                                What it does                                                                                |                                     Guide                                      |
|:-----------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------:|
|  `chain-client`   |                                                A typed, host-routed client for reading on-chain storage, constants, and account state across one or more chains, with no RPC infrastructure to run.                                                 |              [Read On-Chain Data](/apps/build/read-chain-state/)               |
|     `signer`      |                                             Derives product-scoped accounts and requests signatures, routing every approval to the user's Polkadot App. Your Product signs without ever handling keys.                                              |          [Sign and Submit Transactions](/apps/build/sign-and-submit/)          |
|  `cloud-storage`  | A high-level client for the [Bulletin Chain](/reference/apps/infrastructure/bulletin-chain/), Polkadot's content-addressed storage. Uploads and retrieves data by CID, with chunking, manifests, and authorization handled for you. |            [Store Data on Chain](/apps/build/store-data-on-chain/)             |
| `statement-store` | A pub/sub client for the [Statement Store](/reference/apps/infrastructure/statement-store/): publish and subscribe to signed, short-lived statements gossiped peer-to-peer off-chain. Ideal for real-time signaling between users.  | [Publish and Subscribe to Off-Chain Data](/apps/build/pub-sub-off-chain-data/) |
|  `local-storage`  |                                                       A per-Product, per-device key-value store backed by the Host, for preferences, drafts, and cached values that persist across sessions.                                                        |           [Persist Data Locally](/apps/build/persist-data-locally/)            |

These five anchor the current recipes because their surfaces are stable. Other packages in the SDK (`contracts`, `keys`, `crypto`, `host`) will get recipes of their own as their surfaces stabilize.

The guides also use these utility packages where relevant:

- **`tx`**: Builds, signs, and tracks the lifecycle of transactions (used alongside `signer`).
- **`address`**: Encodes, decodes, and validates SS58 addresses.
- **`descriptors`**: Provides typed chain metadata for the `chain-client` Bring Your Own Descriptors path.
- **`host`**: Provides low-level access to the Host API (for example, the preimage manager used for sponsored uploads).


Umbrella or Individual Packages

## Umbrella or Individual Packages

Every guide works with either install style; choose based on your needs:

- **Umbrella package**: `npm install @parity/product-sdk`. One dependency that re-exports everything. Convenient when your Product uses several capabilities and bundle size is not a concern.
- **Individual packages**: `npm install @parity/product-sdk-cloud-storage` (and so on). Install only what you use to keep your bundle smaller and your dependencies explicit.

Import paths are identical either way, so you can start with the umbrella package and switch to individual packages later as a bundle-size optimization.


Introduction

## Introduction

This guide covers the `@parity/product-sdk-local-storage` package, which provides a `LocalKvStore` for storing and retrieving key-value data between sessions. Use it for device-local data like saved preferences, unsubmitted drafts, or cached responses. Data is scoped to your Product and device; it does not sync across devices or leak between Products on the same Host.

!!! info "Storage options for your Product"
    `LocalKvStore` is the right tool for device-local, per-Product key-value data. When your data needs to outlive the device or be visible to other users, reach for an on-chain layer instead:

    - **Local KvStore** (this page): Per-Product, per-device key-value. User preferences, drafts, cached values. Not synced across devices.
    - **Bulletin Chain**: Content-addressed, on-chain, retained ~2 weeks by default and renewable. Content readers fetch later by hash: profile photos, published articles, app bundles. See [Store Data on Chain](/apps/build/store-data-on-chain/).
    - **Statement Store**: Gossip-distributed, short-lived (default 30s TTL), allowance-gated. Real-time signaling between users: chat messages, presence, typing indicators. See [Publish and Subscribe to Off-Chain Data](/apps/build/pub-sub-off-chain-data/).


Prerequisites

## Prerequisites

Before getting started, ensure you have:

- A Polkadot Product project running locally (see [Set Up Your Project](/apps/build/#set-up-your-project))
- Node.js 20 or later with ESM support (`@parity/product-sdk-local-storage` is ESM only)


Install the SDK

## Install the SDK

You have two installation options depending on your needs:

- **Umbrella package** (recommended starting point): Install the full SDK in one command. Convenient when your Product uses several SDK features (local storage, signing, cloud storage, etc.) and bundle size is not a concern.

    ```bash
    npm install @parity/product-sdk
    ```

- **Individual package**: Install only what you use. Keeps your bundle smaller and makes dependencies explicit; switch to this later as a bundle-size optimization.

    ```bash
    npm install @parity/product-sdk-local-storage
    ```

All import paths shown in this guide work with both options.

!!! note "Code examples"
    Each snippet in this guide is a standalone file you add to your Product's source tree. The filenames match the `title` shown in the code block header (for example, `initialize.ts`, `set-get-string.ts`). They are not meant to be concatenated; import and use each one independently wherever it fits in your Product.


Initialize the Store

## Initialize the Store

`createLocalKvStore()` is an async factory. You must `await` it before performing any read or write, because the function connects to the Host storage backend before the store is usable.

```typescript title="initialize.ts"
import { createLocalKvStore } from '@parity/product-sdk-local-storage';
import type { LocalKvStore } from '@parity/product-sdk-local-storage';

// Connects to the Host storage backend. Must run inside a Polkadot Product;
// outside a host container this throws "Host storage unavailable".
const store: LocalKvStore = await createLocalKvStore();
```

`createLocalKvStore()` accepts an optional `LocalKvStoreOptions` argument. The supported option is `prefix`, which is covered in [Namespace Keys with Prefixes](#namespace-keys-with-prefixes).


Store String Values

## Store String Values

Use `store.set(key, value)` to write a string value for a key. Use `store.get(key)` to read it back. If the key has not been set, `get` returns `null`.

```typescript title="set-get-string.ts"
await store.set('network', 'paseo');

const network = await store.get('network');

if (network === null) else {
  console.log('Network:', network);
}
```


Store JSON Values

## Store JSON Values

Use `store.setJSON(key, value)` to write any JSON-serializable value. The store handles serialization for you. Use `store.getJSON<T>(key)` to read it back; the generic type parameter `T` enables type-safe retrieval. If the key does not exist, `getJSON` returns `null`.

```typescript title="set-get-json.ts"
interface UserPreferences {
  theme: 'light' | 'dark';
  language: string;
}

await store.setJSON('preferences', {
  theme: 'dark',
  language: 'en',
} satisfies UserPreferences);

const preferences = await store.getJSON<UserPreferences>('preferences');

if (preferences === null) else {
  console.log('Theme:', preferences.theme);
  console.log('Language:', preferences.language);
}
```


Remove Stored Values

## Remove Stored Values

Use `store.remove(key)` to delete a key. The method does not throw if the key does not exist, so you can call it safely without first checking whether the value is present.

```typescript title="remove.ts"
await store.remove('network');
```


Namespace Keys with Prefixes

## Namespace Keys with Prefixes

Within a Product, multiple features may share the same `LocalKvStore`. Prefixes partition the namespace further to avoid key collisions between those features.

Pass a `prefix` option to `createLocalKvStore()` to prepend `prefix:` to every key operated on by that store instance. For example, `createLocalKvStore({ prefix: 'feature' })` stores the key `'setting'` internally as `'feature:setting'`.

```typescript title="prefixed-store.ts"
import { createLocalKvStore } from '@parity/product-sdk-local-storage';

const settingsStore = await createLocalKvStore({ prefix: 'settings' });
const cacheStore = await createLocalKvStore({ prefix: 'cache' });

// Stored internally as "settings:theme"
await settingsStore.set('theme', 'dark');

// Stored internally as "cache:lastBlock"
await cacheStore.set('lastBlock', '21540000');
```

The Host-enforced Product-level namespace is separate from any developer-defined prefix. The Host's Product namespace is applied on top of your `prefix`, so a key `'setting'` in a `{ prefix: 'feature' }` store ends up stored as something like `'myproduct.dot:feature:setting'`, without you needing to construct that path yourself.


Use React Hooks

## Use React Hooks

If your Product is a React application, `@parity/product-sdk` provides hooks that integrate local storage directly into the React component lifecycle. The hooks read the current value on mount and re-render the component whenever it changes.

Wrap your application in `ProductSDKProvider` once at the root, then call the hooks anywhere inside the tree:

```tsx title="providers.tsx"
'use client';

import { ProductSDKProvider } from '@parity/product-sdk/react';
import type { ReactNode } from 'react';

export function Providers({ children }: { children: ReactNode })
    >
      {children}
    </ProductSDKProvider>
  );
}
```

`useLocalStorageString` manages a plain string value. `useLocalStorage<T>` manages a JSON-serializable value typed by `T`. Both hooks return a tuple of `[currentValue, setValue, { loading, error }]`:

```tsx title="local-storage-hooks.tsx"
'use client';

import {
  useLocalStorage,
  useLocalStorageString,
} from '@parity/product-sdk/react';

interface UserPreferences {
  theme: 'light' | 'dark';
  language: string;
}

function StorageDemo()] =
    useLocalStorageString('network');

  const [
    preferences,
    setPreferences,
    { loading: prefsLoading, error: prefsError },
  ] = useLocalStorage<UserPreferences>('preferences');

  return (
    <div>
      <p>Network: {networkLoading ? 'loading…' : (network ?? 'not set')}</p>
      <button onClick={() => setNetwork('paseo')}>Set network</button>

      <p>
        Theme: {prefsLoading ? 'loading…' : (preferences?.theme ?? 'not set')}
      </p>
      <button onClick={() => setPreferences({ theme: 'dark', language: 'en' })}>
        Set preferences
      </button>

      {prefsError && <p>Error: {prefsError.message}</p>}
    </div>
  );
}
```

To remove a key or clear all storage for your Product, access `app.localStorage` from `useProductSDK()` directly:

```tsx title="remove-and-clear.tsx"
'use client';

import { useProductSDK } from '@parity/product-sdk/react';

function StorageActions()

  async function handleClear()

  return (
    <div>
      <button onClick={handleRemove}>Remove "network"</button>
      <button onClick={handleClear}>Clear all</button>
    </div>
  );
}
```

!!! note
    `app.localStorage.clear()` removes every key scoped to your Product. It is equivalent to calling `remove()` on each key individually.


Limitations

## Limitations

- Storage is not synced across devices. Values written on one Host instance are not visible on another.
- `app.localStorage.clear()` and `store.remove()` are scoped to your Product. You cannot read or modify another Product's keys.
- React hooks (`useLocalStorage`, `useLocalStorageString`) are only available via the umbrella package `@parity/product-sdk`. The standalone `@parity/product-sdk-local-storage` package exposes no React hooks.
- `createLocalKvStore()` requires a Host backend. Running outside a host container, it throws `Host storage unavailable`; there is no browser `localStorage` fallback. Run your Product inside Polkadot Desktop to use local storage.


Where to Go Next

## Where to Go Next

-   Guide **Store Data on Chain**

    ---

    When data must outlive the device and be fetchable by anyone, move it to the Bulletin Chain.

    [:octicons-arrow-right-24: Store Data on Chain](/apps/build/store-data-on-chain/)

-   Guide **Deploy Your Product**

    ---

    You've covered the capabilities; ship your Product to a live `.dot` name with the Quick Start deploy routes.

    [:octicons-arrow-right-24: Quick Start](/apps/quick-start/)

-   External **Product SDK API Reference**

    ---

    The full `product-sdk` surface beyond this recipe: every package, class, and method.

    [:octicons-arrow-right-24: Visit Site](https://paritytech.github.io/product-sdk/)


Introduction

## Introduction

This guide covers the [Statement Store](/reference/apps/infrastructure/statement-store/), a pub/sub primitive on Polkadot's People Chain for real-time signaling between users of your Product. Statements are small, signed payloads that propagate peer-to-peer via the node's gossip layer without entering chain storage, making them the right tool for chat messages, presence indicators, multiplayer cursors, and typing indicators. Submissions are allowance-gated (no fees); reading is permissionless. The guide covers four steps: setting up the client, subscribing to incoming statements, publishing a typed statement, and using a channel for last-write-wins state.

!!! info "Storage options for your Product"
    The Statement Store is the right layer for short-lived signaling between users. When your data must outlive a session or stay on-device, reach for a different layer instead:

    - **Local KvStore**: Per-Product, per-device key-value. User preferences, drafts, cached values. Not synced across devices. See [Persist Data Locally](/apps/build/persist-data-locally/).
    - **Bulletin Chain**: Content-addressed, on-chain, retained ~2 weeks by default and renewable. Content readers fetch later by hash: profile photos, published articles, app bundles. See [Store Data on Chain](/apps/build/store-data-on-chain/).
    - **Statement Store** (this page): Gossip-distributed, short-lived (default 30s TTL), allowance-gated. Real-time signaling between users: chat messages, presence, typing indicators, multiplayer state.

The Statement Store and Bulletin Chain compose well: Polkadot App's Chat uses the Statement Store for signaling (who's online, session handshakes) and the Bulletin Chain for the encrypted message content. Many Products follow the same split: Statement Store for ephemeral state, Bulletin Chain for content that needs to survive longer than a session.


Prerequisites

## Prerequisites

Before starting, ensure you have:

- Completed [Install Desktop and Pair](/apps/get-started/) so you have a paired Polkadot Desktop and a signer for product-scoped accounts
- A Statement Store allowance for your account; the People Chain's `pallet-statement-store` gates submissions on a per-account allowance (`max_count` live statements and `max_size` total bytes)
- A Polkadot Product project running locally (see [Set Up Your Project](/apps/build/#set-up-your-project) if you don't have one yet)

!!! warning "Provisional: obtaining an allowance"
    The process for obtaining a Statement Store allowance on TestNet is not yet documented. Ask in the developer community for the current access paths.

!!! note
    PAS funds are not required for the Statement Store itself; submissions don't pay transaction fees. The allowance is the gating mechanism; PAS is only relevant if your Product later interacts with chains that charge fees.


Install the SDK

## Install the SDK

Install the SDK at pinned versions so the snippets type-check predictably. Lead with the umbrella `@parity/product-sdk` (the recommended starting point), alongside the `statement-store` package the snippets import directly:

```bash
npm install @parity/product-sdk@0.8.0 @parity/product-sdk-statement-store@0.4.0
```

The umbrella package provides `createApp` and re-exports the rest of the SDK; switch to individual packages later as a bundle-size optimization. See [Umbrella or Individual Packages](/apps/build/#umbrella-or-individual-packages) for the tradeoff.


Set Up Your Statement Store Client

## Set Up Your Statement Store Client

Every snippet in this guide is Product code: modules placed inside the Product running at `localhost:3000` (per [Set Up Your Project](/apps/build/#set-up-your-project)), loaded by Polkadot Desktop. Signing requests route through the Host (Polkadot Desktop) to the user's paired Polkadot App on their phone, which holds the signing keys; your Product never derives, sees, or holds keys.

`StatementStoreClient` is the high-level client. It wraps the People Chain node's `statement_submit` and `statement_subscribeStatement` JSON-RPC methods, handles JSON encoding of your payload, requests authentication proofs from the Host, and deduplicates incoming statements. Connect it once at the top of your Product:

```typescript title="setup-statement-store.ts"
import { createApp } from '@parity/product-sdk';
import { StatementStoreClient } from '@parity/product-sdk-statement-store';

const app = await createApp({ name: 'my-product' });
const { accounts } = await app.wallet.connect();
if (accounts.length === 0)

export const client = new StatementStoreClient({ appName: 'my-product' });
await client.connect({
  mode: 'host',
  accountId: [accounts[0].address, 42], // 42 = generic SS58 prefix
});

console.log('Statement Store connected as', accounts[0].address);
```

`new StatementStoreClient({ appName })` creates the client. The `appName` is hashed with Blake2b-256 and used as the statement's primary topic; every submission your Product makes carries that topic, and `client.subscribe()` filters on it at the node boundary, so other instances of _your_ Product see your statements while traffic from other Products on the network is dropped before it reaches your subscriber. `client.connect({ mode: 'host', accountId })` wires the client to the Host API transport and delegates proof creation to the Host; Polkadot Desktop routes the proof request to the user's paired Polkadot App, which signs and returns the proof. The `accountId` is a tuple of `[ss58Address, chainPrefix]`; `42` is the generic Substrate SS58 prefix.

!!! note "Delivery is best-effort"
    The Statement Store does not retry, acknowledge, or guarantee ordering; those are network-layer guarantees the gossip protocol doesn't provide. If your Product needs to know a statement reached a peer, the peer publishes an ack statement; reliability is composed at the application layer. If the content needs to outlive the TTL, store it on the Bulletin Chain and publish a CID as the statement payload.

    Polkadot App's Chat is exactly this composition: Statement Store for signaling, Bulletin Chain for the encrypted message content.


Subscribe to Incoming Statements

## Subscribe to Incoming Statements

`client.subscribe<T>(callback, options?)` registers a topic filter with the connected node. Internally, the SDK calls `statement_subscribeStatement` with a filter scoped to your Product's `appName` topic; the node returns the current statement pool matching that filter and streams any further submissions that match. The SDK decodes the JSON payload into your typed `T` and dedupes by channel and content hash before invoking your callback:

```typescript title="subscribe-statements.ts"
// Place this in your Product, after the setup from `setup-statement-store.ts`.

import type { ReceivedStatement } from '@parity/product-sdk-statement-store';
import { client } from './setup-statement-store';

// JSON-serialized when published; decoded back to this type on receive.
interface ChatMessage {
  text: string;
  from: string;
  ts: number;
}

const subscription = client.subscribe<ChatMessage>(
  (statement: ReceivedStatement<ChatMessage>) => {
    console.log(
      `[${statement.signerHex?.slice(0, 10)}…] ${statement.data.from}: ${statement.data.text}`,
    );
  },
  { topic2: 'room-42' },
);

// subscription.unsubscribe() when you no longer want updates.
```

The optional `topic2` is hashed with Blake2b-256 and added to the filter; scope to a room id, a document id, or any other secondary key within your Product. The returned `Unsubscribable` exposes `unsubscribe()` to tear down the JSON-RPC subscription when you no longer need it.

!!! warning "A subscription receives every statement on its topic, regardless of payload shape"
    The filter matches on topics, not on your TypeScript type. A subscriber on a given `topic2` receives _all_ statements published to that topic, including channel writes (the next section) and any other payload type your Product publishes there. The SDK decodes each one as your declared `T`, so a statement with a different shape arrives with `undefined` fields rather than an error.

    If your Product publishes more than one kind of payload (for example, chat messages _and_ presence updates), either give each kind its own `topic2` (`room-42-chat` vs `room-42-presence`) or include a discriminator field in the payload and branch on it inside the callback. Reusing one `topic2` for distinct shapes will cross-deliver them.


Publish a Typed Statement

## Publish a Typed Statement

`client.publish<T>(data, options?)` builds a `Statement`, JSON-encodes the payload, requests a proof through the Host (Polkadot Desktop forwards the request to the user's paired Polkadot App, which signs), and submits via the node's `statement_submit` JSON-RPC. Every instance of your Product subscribed to the matching topic will receive it as the gossip propagates:

```typescript title="publish-statement.ts"
// Place this in your Product, after the setup from `setup-statement-store.ts`.

import { client } from './setup-statement-store';

interface ChatMessage {
  text: string;
  from: string;
  ts: number;
}

const accepted = await client.publish<ChatMessage>(
  {
    text: 'Hello, room!',
    from: 'alice',
    ts: Date.now(),
  },
  {
    topic2: 'room-42', // scope to a specific room, doc, or context
    ttlSeconds: 60, // override the default 30s TTL
  },
);

if (accepted) else {
  console.warn('Statement rejected by the network');
}
```

`publish` returns `Promise<boolean>`: `true` when the node accepted the statement into its pool, `false` when it was rejected by `pallet-statement-store`'s validity check (typically allowance, size, or proof failure). It throws `StatementDataTooLargeError` if the JSON-encoded payload exceeds the per-statement size limit (512 bytes), and `StatementConnectionError` if the client is not connected.

Two limits worth designing around:

- **512-byte payload limit**: Measured after JSON encoding. For larger content, store it on the Bulletin Chain and publish the CID as a small statement.
- **30-second default TTL**: The pallet enforces a maximum retention window. The SDK defaults to 30 seconds, and you can shorten or lengthen per-statement via `ttlSeconds` up to that cap. After expiry, the node evicts the statement from its pool.

`PublishOptions` covers the common cases: `topic2` (secondary Blake2b-256 topic for room/doc scoping), `channel` (last-write-wins; see the next section), `ttlSeconds` (override the default), and `decryptionKey` (an opaque hint subscribers can match on to discover encrypted content).


Use a Channel for Last-Write-Wins State

## Use a Channel for Last-Write-Wins State

`pallet-statement-store` supports an optional `channel` field on every statement: when a new submission carries the same `channel` as an existing live statement from the same account, the pallet replaces the older one in the node's pool. The gossip layer then propagates the replacement, and every subscribed peer drops the old statement in favor of the new one. The SDK abstracts this with `ChannelStore<T>`; each channel name maps to a single live value:

```typescript title="channel-presence.ts"
// Place this in your Product, after the setup from `setup-statement-store.ts`.

import { ChannelStore } from '@parity/product-sdk-statement-store';
import { client } from './setup-statement-store';

interface Presence {
  status: 'online' | 'away' | 'offline';
  timestamp: number;
}

const channels = new ChannelStore<Presence>(client, { topic2: 'room-42' });

await channels.write('presence/alice', {
  status: 'online',
  timestamp: Date.now(),
});

// A second write on the same channel replaces the first.
await channels.write('presence/alice', {
  status: 'away',
  timestamp: Date.now(),
});

channels.onChange((name, value, previous) => {
  console.log(`${name}: ${previous?.status ?? '<none>'} → ${value.status}`);
});

for (const [name, value] of channels.readAll()): ${value.status}`);
}
```

`channels.write(name, value)` hashes the channel name with Blake2b-256 and publishes; `channels.read(name)` returns the latest value seen on that channel; `channels.readAll()` returns the full map; `channels.onChange(callback)` fires on every transition. `ChannelStore` stamps `timestamp` for you if the value omits it. Channel scope is per-account; the pallet's replacement rule only matches statements from the same signer, so one user cannot overwrite another user's channel.

`ChannelStore` is the right primitive for soft state where only the latest version matters: presence indicators, multiplayer cursors, "now playing" status. For append-only events such as chat messages, action logs, social-feed posts, keep using `client.publish` directly so each event lives independently until its TTL.


Where to Go Next

## Where to Go Next

-   Guide **Persist Data Locally**

    ---

    Your Product can sync state between users; next, keep device-local data (preferences, drafts, caches) across sessions.

    [:octicons-arrow-right-24: Persist Data Locally](/apps/build/persist-data-locally/)

-   Guide **Store Data on Chain**

    ---

    For durable, content-addressed payloads, route through the Bulletin Chain. Pair with the Statement Store by publishing the CID as a Statement.

    [:octicons-arrow-right-24: Store Data on Chain](/apps/build/store-data-on-chain/)

-   External **Product SDK API Reference**

    ---

    The full `product-sdk` surface beyond this recipe: every package, class, and method.

    [:octicons-arrow-right-24: Visit Site](https://paritytech.github.io/product-sdk/)


Introduction

## Introduction

This guide covers the `@parity/product-sdk-chain-client` package, which gives your Product a typed, host-aware client for reading on-chain state (account balances, storage items, and runtime constants) from inside a Polkadot host container. The SDK ships two connection paths: a zero-config Preset path (`getChainAPI`) and a Bring Your Own Descriptors (BYOD) path (`createChainClient`) for explicit descriptor control. Both produce the same client shape.


Prerequisites

## Prerequisites

Before starting, ensure you have:

- A Polkadot Product project running locally (see [Set Up Your Project](/apps/build/#set-up-your-project)) with a TypeScript toolchain
- Node.js 20 or later with ESM support (`@parity/product-sdk-chain-client` is ESM only)
- Polkadot Desktop to run your Product inside a host container (see [Install Desktop and Pair](/apps/get-started/))

!!! note
    You do not need funded accounts to read chain state. Reads are unsigned and require only a running host container.


Install the SDK

## Install the SDK

You have two installation options depending on your needs:

- **Umbrella package** (recommended starting point): Install the full SDK in one command. Convenient when your Product uses several SDK features (chain client, signing, cloud storage, etc.) and bundle size is not a concern.

    ```bash
    npm install @parity/product-sdk
    ```

- **Individual packages**: Install only what you use. Keeps your bundle smaller and makes dependencies explicit; switch to this later as a bundle-size optimization.

    ```bash
    npm install @parity/product-sdk-chain-client
    ```

All import paths shown in this guide work with both options.

If you plan to use the BYOD path, also install the descriptors package regardless of which option above you chose:

```bash
npm install @parity/product-sdk-descriptors
```

The descriptors package exposes typed `ChainDefinition` objects through subpath imports (for example, `@parity/product-sdk-descriptors/paseo-bulletin`). Each subpath corresponds to one chain.


Connect to a Chain

## Connect to a Chain

The SDK provides two connection paths. The Preset path (`getChainAPI`) is the fastest way to get a working client — it comes preconfigured with descriptors and RPC endpoints for supported environments. Use the BYOD path (`createChainClient`) when you need explicit control over which chains and descriptors your Product uses.


Connect Using a Preset

### Connect Using a Preset

`getChainAPI(env)` returns a client preconfigured with the descriptors and RPC endpoints for the requested environment.

```typescript
import { getChainAPI } from '@parity/product-sdk-chain-client';

const client = await getChainAPI('paseo');

const fee = await client.bulletin.query.TransactionStorage.ByteFee.getValue();
const blockNumber = await client.assetHub.query.System.Number.getValue();

client.destroy();
```

The returned client exposes one property per chain in the preset (`assetHub`, `bulletin`, `individuality`), each typed by the underlying [polkadot-api](https://papi.how) (PAPI) descriptor.

!!! warning "Not every environment is live"
    `getChainAPI` accepts the `Environment` values `polkadot`, `kusama`, `paseo`, `summit`, `local`, and `westend`. Only `paseo` and `summit` are wired up at the moment; calling an environment that is not yet live throws at runtime.


Connect Using Custom Descriptors (BYOD)

### Connect Using Custom Descriptors (BYOD)

Use `createChainClient` to supply your own descriptors and RPC endpoints, importing the chain descriptor objects you need directly instead of relying on a preset.

```typescript
import { createChainClient } from '@parity/product-sdk-chain-client';
import {
  paseo_asset_hub,
} from '@parity/product-sdk-descriptors/paseo-asset-hub';
import {
  paseo_bulletin,
} from '@parity/product-sdk-descriptors/paseo-bulletin';

const client = await createChainClient({
  chains: {
    assetHub: paseo_asset_hub,
    bulletin: paseo_bulletin,
  },
});

client.destroy();
```

The keys you choose in `chains` (`assetHub`, `bulletin`) become the property names on the returned client. Pick names that read naturally in your call sites; the rest of the SDK is fully typed against them. Connections are routed through the host at runtime, so you don't supply RPC endpoints yourself.

!!! tip "Using a different chain"
    To connect to a chain other than `paseo_bulletin`, find its descriptor in `@parity/product-sdk-descriptors`, then add it under a new key in `chains`. The client surface (`client.<yourKey>.query.*`) is automatically typed to match the descriptor you supplied.


Read Storage and Constants

## Read Storage and Constants

Once you have a client, every chain you connected exposes a typed PAPI surface under `.query`, `.constants`, and the rest of the PAPI shape. The pattern is:

```text
client.<chainName>.query.<Pallet>.<Item>.getValue(...args)
```

The following examples show common read operations. Each one maps directly to a pallet storage item and requires no transaction or signature.

??? code "Byte Fee"
    `TransactionStorage.ByteFee` returns the current cost per byte for on-chain storage on the Bulletin chain.

    ```typescript
    const fee = await client.bulletin.query.TransactionStorage.ByteFee.getValue();
    console.log(`${fee} planck/byte`);
    ```

??? code "Block Number"
    `System.Number` returns the current best block number for the chain.

    ```typescript
    const blockNumber = await client.assetHub.query.System.Number.getValue();
    ```

??? code "Account State"
    `System.Account` returns the full account record for an address, including nonce and balance buckets.

    ```typescript
    const address = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';

    const account = await client.assetHub.query.System.Account.getValue(address);
    const {
      nonce,
      data: { free, reserved, frozen },
    } = account;
    ```


Read Multiple Chains in Parallel

## Read Multiple Chains in Parallel

Because each chain has its own connection internally, reads across chains are independent and can be issued in parallel with `Promise.all`.

```typescript
const [fee, blockNumber] = await Promise.all([
  client.bulletin.query.TransactionStorage.ByteFee.getValue(),
  client.assetHub.query.System.Number.getValue(),
]);
```

This is the recommended pattern whenever your Product needs to assemble a view from more than one storage item or chain.


Access the Raw PAPI Client

## Access the Raw PAPI Client

For advanced flows that the typed surface does not cover, such as raw storage subscriptions, low-level block tracking, or building a `ContractRuntime` for pallet-revive, every chain on the client exposes the underlying `PolkadotClient` under `.raw`.

```typescript
// Subscribe to every new finalized block on the Bulletin chain
const subscription = client.raw.bulletin.finalizedBlock$.subscribe((block) => {
  console.log(`Finalized block #${block.number}: ${block.hash}`);
});

// Unsubscribe when done
subscription.unsubscribe();
```

Use the raw client only when you need to step outside the typed query/constants shape. Most reads should go through the typed surface above.


Detect Whether You Are Inside a Host

## Detect Whether You Are Inside a Host

The SDK re-exports two helpers from `@parity/product-sdk-host` for detecting whether your Product is running inside a host container. Use these when behavior should branch, for example, showing a connection status indicator.

```typescript
import {
  isInsideContainer,
  isInsideContainerSync,
} from '@parity/product-sdk-chain-client';

const inContainer = await isInsideContainer(); // async, canonical
const inContainerSync = isInsideContainerSync(); // sync, for top-level guards
```

!!! note
    You generally do not need to branch on this. The client routes calls correctly when inside a host container; detection is for cases where your Product itself wants to adapt its UI or telemetry.


Clean Up Connections

## Clean Up Connections

Each client owns one or more connections. Tear them down when your Product no longer needs them, typically on view unmount or process shutdown.

```typescript
import { destroyAll } from '@parity/product-sdk-chain-client';

// Close only the connections owned by this client
client.destroy();

// Global escape hatch: closes every connection the SDK has open
destroyAll();
```

Use `client.destroy()` for normal cleanup and reserve `destroyAll()` for full-process teardown.


Limitations

## Limitations

- The `paseo` and `summit` environments are the only presets wired up today. Other `Environment` values throw at runtime.
- The package is ESM only; your Product's build pipeline must support ESM imports.
- Descriptors are imported by subpath (`@parity/product-sdk-descriptors/paseo-bulletin`), not from the package root. Bundlers that do not honor `exports` subpaths will fail to resolve them.
- Host-routed reads require a host container. Development builds outside a Host can use the direct WebSocket fallback.
- Reactive subscriptions (watching a storage item over time) are not covered on this page. A dedicated page on subscriptions will follow.


Where to Go Next

## Where to Go Next

-   Guide **Sign and Submit Transactions**

    ---

    Your Product can read the chain; next, let it act on the user's behalf, with every approval on their phone.

    [:octicons-arrow-right-24: Sign and Submit Transactions](/apps/build/sign-and-submit/)

-   External **polkadot-api Docs**

    ---

    Reference for the typed PAPI surface that `@parity/product-sdk-chain-client` exposes per chain.

    [:octicons-arrow-right-24: Visit Site](https://papi.how)

-   External **Product SDK API Reference**

    ---

    The full `product-sdk` surface beyond this recipe: every package, class, and method.

    [:octicons-arrow-right-24: Visit Site](https://paritytech.github.io/product-sdk/)


Introduction

## Introduction

This guide covers the `@parity/product-sdk-signer` package, which gives your Product a typed, host-aware signing interface for deriving accounts and signing payloads (raw bytes or full transactions) from inside a Polkadot host container. The SDK exposes a single orchestrator, `SignerManager`, that wraps both the production host path (Polkadot Desktop) and a deterministic dev path behind the same `Result`-typed API.


Prerequisites

## Prerequisites

Before starting, ensure you have:

- A Polkadot Product project running locally (see [Set Up Your Project](/apps/build/#set-up-your-project)) with a TypeScript toolchain
- Node.js 20 or later with ESM support (`@parity/product-sdk-signer` is ESM only)
- Polkadot Desktop to run your Product inside a host container (see [Install Desktop and Pair](/apps/get-started/))

!!! note
    To test signing without a host container, use `manager.connect('dev')`. This loads the standard Substrate dev accounts (Alice, Bob, and others) locally and does not require Polkadot Desktop. See [Test Without a Host](#test-without-a-host).

!!! note "Code examples"
    The snippets in this guide are standalone; each one illustrates one concept and can be pasted into your Product independently. They are confirmed working with Polkadot Desktop and `@parity/product-sdk` v0.9.0.


Install the SDK

## Install the SDK

You have two installation options depending on your needs:

- **Umbrella package** (recommended starting point): Install the full SDK in one command. Convenient when your Product uses several SDK features (signing, chain client, cloud storage, and more) and bundle size is not a concern.

    ```bash
    npm install @parity/product-sdk
    ```

- **Individual package**: Install only the signer. Keeps your bundle smaller and makes dependencies explicit; switch to this later as a bundle-size optimization.

    ```bash
    npm install @parity/product-sdk-signer
    ```

All import paths shown in this guide work with both options.


How Product Accounts Work

## How Product Accounts Work

Every product-scoped account is identified by a `ProductAccountId` tuple:

- **`dotNsIdentifier`**: The dotNS identifier of the Product requesting the account, for example the `.dot` name your Product is loaded under.
- **`derivationIndex`**: A non-negative integer chosen by the Product. Index `0` is the conventional default; higher indices give you additional sub-accounts within the same Product.

The same `(dotNsIdentifier, derivationIndex)` pair always yields the same public key, so your Product can reproduce the same address across sessions without persisting it.

!!! note "Per-Product isolation is a privacy feature"
    `app-a.dot` and `app-b.dot` produce different addresses for the same user. This prevents passive on-chain correlation across Products. Sharing an account across Products requires explicit permission; it is never the default.


Set Up Signer Manager

## Set Up Signer Manager

Construct `SignerManager` once and hold it for the lifetime of your Product. Always set `ss58Prefix` and `dappName` explicitly. Use the `onConnect` callback to request any resources your Product needs immediately after connection; the callback fires exactly once per connect and re-fires automatically after any auto-reconnect.

```typescript
import { SignerManager } from '@parity/product-sdk-signer';

const manager = new SignerManager({
  ss58Prefix: 0,
  dappName: 'my-product',
  onConnect: async (_account, { requestResourceAllocation }) => {
    await requestResourceAllocation([{ tag: 'AutoSigning', value: undefined }]);
  },
});
```

- **`ss58Prefix`**: The SS58 address-format prefix for the target network. Use `0` for the Polkadot relay chain.
- **`dappName`**: A human-readable name for your Product, shown in Desktop UI.
- **`onConnect`**: Fires once per connection transition. Use `ctx.requestResourceAllocation` to request permissions such as `AutoSigning` up front, before any signing call is made.


Connect to the Host

## Connect to the Host

Call `connect()` to establish a session with the host and discover available accounts. `connect()`, `selectAccount()`, `getProductAccount()`, and `signRaw()` all return a `Result`; always check `.ok` before accessing `.value`. (`getSigner()` returns a nullable directly, not a `Result`.)

```typescript
const connectResult = await manager.connect();
if (!connectResult.ok)

// Auto-select the first account if none is already selected
const accounts = connectResult.value;
if (accounts.length > 0 && !manager.getState().selectedAccount)
}
```

`connect()` defaults to the host provider. Outside a host container it returns `HostUnavailableError`; use `connect('dev')` for local testing instead. See [Test Without a Host](#test-without-a-host).


Get a Product Account

## Get a Product Account

`getProductAccount` requests the product-scoped account for a given `dotNsIdentifier` from the host. This is a host-only API; it returns `HostUnavailableError` when the active provider is `'dev'`.

```typescript
const accountResult = await manager.getProductAccount('my-product.dot', 0);
if (!accountResult.ok)

const { address, publicKey } = accountResult.value;
```

The returned `SignerAccount` exposes:

- **`address`**: The SS58-encoded address for the current network.
- **`publicKey`**: The raw 32-byte public key.
- **`name`**: An optional display name, or `null`.


Sign Arbitrary Bytes

## Sign Arbitrary Bytes

Use `signRaw` to sign an arbitrary byte payload with the currently selected account. This is useful for off-chain authentication, message proofs, and any use case that does not require a full transaction.

`signRaw` returns a `Result<Uint8Array, SignerError>`; it never throws. Always check `.ok` before accessing `.value`.

```typescript
const selectResult = manager.selectAccount(accountResult.value.address);
if (!selectResult.ok)

const payload = new TextEncoder().encode('hello polkadot');
const result = await manager.signRaw(payload);

if (result.ok) else {
  console.error(result.error.message);
}
```


Sign and Submit a Transaction

## Sign and Submit a Transaction

To sign a transaction, get a `PolkadotSigner` from `SignerManager` and pass it to the `signAndSubmit` method from `polkadot-api` (PAPI). The SDK handles routing the signing request to Desktop, which renders a signing modal and forwards to the Polkadot App.

`@parity/product-sdk-signer` provides only the signer interface; chain connectivity uses `polkadot-api` directly. Set up your PAPI client separately:

```typescript
import { createClient } from 'polkadot-api';
import { getWsProvider } from 'polkadot-api/ws-provider/web';
import { dot } from '@polkadot-api/descriptors';

const client = createClient(getWsProvider('wss://rpc.polkadot.io'));
const api = client.getTypedApi(dot);
```

The `dot` descriptor is the typed API surface for the Polkadot relay chain. Run `npx papi add dot` in your project to pull or regenerate it.

```typescript
const accountResult = await manager.getProductAccount('my-product.dot', 0);
if (!accountResult.ok) return;

const selectResult = manager.selectAccount(accountResult.value.address);
if (!selectResult.ok) return;

// getSigner() returns null if no account is selected
const signer = manager.getSigner();
if (!signer) return;

const recipient = 'INSERT_RECIPIENT_ADDRESS';
const tx = api.tx.Balances.transfer_keep_alive({
  dest: recipient,
  value: 1_000_000_000_000n,
});

const result = await tx.signAndSubmit(signer);
```


Handle Signing Errors

## Handle Signing Errors

Two outcomes are worth handling explicitly: the user rejects the request, or the signing session times out.

```typescript
import {
  HostRejectedError,
  TimeoutError,
} from '@parity/product-sdk-signer';

try {
  const result = await tx.signAndSubmit(signer);
} catch (error)
  if (error instanceof TimeoutError)
  throw error;
}
```

- **`HostRejectedError`**: The user explicitly denied the request. Return the user to a stable state and let them retry.
- **`TimeoutError`**: The signing session expired. Treat this the same as a rejection.

!!! tip "Design for async latency"
    Signing is asynchronous because the Polkadot App runs on a separate device. Show a non-blocking pending state rather than freezing the interface, and make sure your retry path is idempotent in case the user attempts the same action twice.


Test Without a Host

## Test Without a Host

During development, use `connect('dev')` to load the standard Substrate dev accounts (Alice, Bob, Charlie, Dave, Eve, and Ferdie) without needing Polkadot Desktop. The signing API is identical — only the provider changes.

```typescript
const result = await manager.connect('dev');
if (!result.ok) return;

const selectResult = manager.selectAccount(result.value[0].address);
if (!selectResult.ok) return;

const signResult = await manager.signRaw(new TextEncoder().encode('test'));
if (signResult.ok)
```

!!! warning
    `getProductAccount`, `getProductAccountAlias`, and `createRingVRFProof` are host-only APIs. They return `HostUnavailableError` when the active provider is `'dev'`.


Limitations

## Limitations

- `getProductAccount`, `getProductAccountAlias`, and `createRingVRFProof` require an active host connection. They are not available in `'dev'` mode.
- The package is ESM only; your Product's build pipeline must support ESM imports.
- `SignerManager.destroy()` is terminal. After calling it, all subsequent method calls return `DestroyedError`. Use `disconnect()` for a reversible reset.
- Account persistence across page reloads requires the host to expose `localStorage`. Outside a host container, persistence silently no-ops.


Where to Go Next

## Where to Go Next

-   Guide **Store Data on Chain**

    ---

    Your Product can sign; next, use that signer to store content on the Bulletin Chain and fetch it from anywhere by CID.

    [:octicons-arrow-right-24: Store Data on Chain](/apps/build/store-data-on-chain/)

-   External **PAPI Docs**

    ---

    Reference for the typed PAPI signer interface that `@parity/product-sdk-signer` exposes.

    [:octicons-arrow-right-24: Visit Site](https://papi.how)

-   External **Product SDK API Reference**

    ---

    The full `product-sdk` surface beyond this recipe: every package, class, and method.

    [:octicons-arrow-right-24: Visit Site](https://paritytech.github.io/product-sdk/)


Introduction

## Introduction

This guide covers the [Bulletin Chain](/reference/apps/infrastructure/bulletin-chain/), Polkadot's content-addressed storage layer for Products. You write data, the chain returns a Content Identifier (CID), and anyone with that CID can fetch the data back from the network. Data is retained for about two weeks by default and can be renewed. Access is gated by a per-account storage authorization, not a token balance. The guide walks through five flows in order of complexity: a Hello World store and retrieve, a larger file upload, long-lived data with renewal, cross-chain storage via People Chain, and Preimage submission.

!!! info "Storage options for your Product"
    The Bulletin Chain is the right layer for content that needs to outlive a session and be fetched later by hash. For other shapes of data, reach for a different layer:

    - **Local KvStore** (this page): Per-Product, per-device key-value. User preferences, drafts, cached values. Not synced across devices.
    - **Bulletin Chain**: Content-addressed, on-chain, retained ~2 weeks by default and renewable. Content readers fetch later by hash: profile photos, published articles, app bundles. See [Store Data on Chain](/apps/build/store-data-on-chain/).
    - **Statement Store**: Gossip-distributed, short-lived (default 30s TTL), allowance-gated. Real-time signaling between users: chat messages, presence, typing indicators. See [Publish and Subscribe to Off-Chain Data](/apps/build/pub-sub-off-chain-data/).

!!! note "Network"
    The flows on this page target Paseo Next, the default environment in Polkadot Desktop development builds. If you switched environments during setup, select the matching network from the environment selector in Polkadot Desktop.


Prerequisites

## Prerequisites

Before starting, ensure you have:

- Completed the [Install Desktop and Pair](/apps/get-started/) and [Get TestNet Tokens](/apps/get-started/get-testnet-tokens/) guides; your account needs PAS funds and a Bulletin Chain authorization. If you have not obtained a Bulletin Chain authorization yet, request one from the [Bulletin Chain authorization page](https://paritytech.github.io/polkadot-bulletin-chain/authorizations)
- A Polkadot Product project running locally (see [Set Up Your Project](/apps/build/#set-up-your-project))


Install the SDK

## Install the SDK

Install the SDK in your Product's project:

```bash
npm install @parity/product-sdk
```

The umbrella package brings in `@parity/product-sdk-cloud-storage` (where `CloudStorageClient` lives, used later for advanced operations), `@parity/product-sdk-host` (the Preimage manager used later), and `polkadot-api` itself. To keep your bundle smaller, you can install those individual packages directly instead. See [Umbrella or Individual Packages](/apps/build/#umbrella-or-individual-packages) for the tradeoff.


Set Up Your Storage Client

## Set Up Your Storage Client

Every snippet in this guide is Product code: modules you place inside the Product running at `localhost:3000` (per [Set Up Your Project](/apps/build/#set-up-your-project)), loaded by Polkadot Desktop. Run nothing from a terminal; the snippets execute in the browser context Polkadot Desktop's localhost bypass loads them into. The signer for every Bulletin transaction comes from the Host (your paired account in Polkadot Desktop); your Product never derives keys.

Create the SDK app and connect the wallet:

```typescript title="setup-app.ts"
import { createApp } from '@parity/product-sdk';

export const app = await createApp({ name: 'my-product' });
await app.wallet.connect();
```

`createApp({ name })` returns an `App` with `app.wallet`, `app.localStorage`, `app.chain`, and `app.cloudStorage`, the high-level Bulletin Chain API exposing `upload()`, `fetch()`, and `computeCid()`. The exported `app` is reused across the simple sections that follow.


Store a Hello World

## Store a Hello World

The simplest write: a short string, one line.

```typescript title="hello-bulletin.ts"
// Place this in your Product, after the setup from `setup-app.ts`.

import { app } from './setup-app';

const cid = await app.cloudStorage!.upload('Hello, Bulletin!');

console.log(`CID: ${cid}`);
```

`app.cloudStorage.upload(data)` accepts a string or `Uint8Array`, signs the underlying transaction with your paired account, and resolves with the CID, a Blake2b-256 content hash encoded as a CIDv1 string. Internally the SDK uses the chunking pipeline with a DAG-PB manifest, so the same call shape works for any payload size; see [Store a Larger File](#store-a-larger-file) for the chunk-level controls.

You should see something like:

```text
CID: bafk2bzacea6wlxyalo6gbajlwuubv7w5dvss3vmfqmavlqy63e4vypth2ov6u
```


Retrieve Your Data

## Retrieve Your Data

The Bulletin Chain follows a "write-to-chain, read-from-network" model: the chain holds the storage commitment, and the data itself lives at collator nodes addressable by CID. Reading is permissionless. No authorization, fees, or signature are required. `app.cloudStorage.fetch(cid)` routes through the Host's preimage subscription with caching:

```typescript title="retrieve-data.ts"
// Place this in your Product, after the setup from `setup-app.ts`.

import { app } from './setup-app';

const CID_STRING = 'INSERT_CID';

const bytes = await app.cloudStorage!.fetch(CID_STRING);
console.log(`Retrieved ${bytes.length} bytes`);
console.log(new TextDecoder().decode(bytes));

// Verify the fetched bytes match the CID you asked for.
const recomputed = await app.cloudStorage!.computeCid(bytes);
console.log(`CID verified: ${recomputed === CID_STRING}`);
```

The snippet verifies the bytes by recomputing their CID with `app.cloudStorage.computeCid(bytes)` and comparing to the CID you asked for. If the host returned the wrong bytes, the recomputed CID does not match; that is the integrity property content addressing gives you.

For libp2p / Helia / Smoldot retrieval paths (when you want to fetch outside a Polkadot Desktop container), see [Retrieve Your Data](/chain-interactions/store-data/bulletin-chain/#retrieve-your-data) in the canonical tutorial.


Store a Larger File

## Store a Larger File

`app.cloudStorage.upload()` chunks transparently above a 2 MiB threshold and stores a DAG-PB manifest that references each chunk's CID, returning the manifest CID. For most Products, that is all you need. Pass any `Uint8Array` to `upload()` and the SDK handles chunking, manifest generation, and the underlying transactions for you.

For finer control, such as custom chunk size, per-chunk progress callbacks, or access to the individual chunk CIDs, drop one level lower to `CloudStorageClient` from `@parity/product-sdk-cloud-storage`. This is also the path you use for the next two sections (authorization checks, renewal), so the setup snippet pays off immediately:

```typescript title="setup-client.ts"
import { SignerManager } from '@parity/product-sdk';
import { CloudStorageClient } from '@parity/product-sdk-cloud-storage';

const signerManager = new SignerManager({ dappName: 'my-product' });
await signerManager.connect();

const { accounts } = signerManager.getState();
if (accounts.length === 0)

// A real Product would render an account picker; here we pick the first one.
signerManager.selectAccount(accounts[0].address);

const signer = signerManager.getSigner();
if (!signer) throw new Error('Could not build a signer from the selected account.');

export const client = await CloudStorageClient.create({
  environment: 'paseo',
  signer,
});

export const account = signerManager.getState().selectedAccount!;
```

`CloudStorageClient.create({ environment: 'paseo', signer })` resolves with `client.store / client.renew / client.checkAuthorization / client.fetchBytes / client.estimateAuthorization` for the high-level operations, plus `client.api` for the typed Bulletin Chain API when you need to drop one level lower still. The exported `client` and `account` are reused by the advanced sections that follow.

The `client.store(...)` builder takes the same chunking pipeline `app.cloudStorage.upload` uses internally, with the controls exposed:

```typescript title="store-large-file.ts"
// Place this in your Product, after the setup from `setup-client.ts`.

import { client } from './setup-client';

// e.g., a File the user dropped, an asset bundled with your Product.
declare const largeFile: Uint8Array;

const estimate = client.estimateAuthorization(largeFile.length);
console.log(
  `Need ${estimate.transactions} txs / ${estimate.bytes} bytes of authorization`,
);

const result = await client
  .store(largeFile)
  .withChunkSize(1024 * 1024)
  .withManifest(true)
  .withCallback((event) => {
    console.log(`Progress: ${JSON.stringify(event)}`);
  })
  .send();

console.log(`Root CID (manifest): ${result.cid?.toString()}`);
if (result.chunks)`);
  for (const [i, cid] of result.chunks.chunkCids.entries())] ${cid.toString()}`);
  }
}
```

The returned `StoreResult.cid` is the manifest CID; `StoreResult.chunks.chunkCids` lists each chunk's individual CID. `withCallback()` receives per-chunk progress events as the upload streams. The Bulletin Chain has a per-transaction byte limit of about 8 MiB on TestNet; see [Size Limits](/reference/polkadot-hub/data-storage/#size-limits). Chunks must stay under that.

Chunked uploads are not atomic. Each chunk is a separate transaction, and the manifest is one more on top. If chunk N fails after chunks 0..N-1 have already landed, the earlier chunks remain on chain and consume your authorization. There is no rollback. Inspect the `chunkCids` on the thrown error and either resume from the failed chunk or, if the use case demands it, abandon the partial upload.


Get Authorization

## Get Authorization

The Bulletin Chain has no token balance for storage; every account needs an explicit authorization. You should already have one from [Get TestNet Tokens](/apps/get-started/get-testnet-tokens/); if not, request your storage quota directly from the [Bulletin Chain authorization page](https://paritytech.github.io/polkadot-bulletin-chain/authorizations).

!!! note
    The `authorize_account` extrinsic requires Root origin. You cannot self-authorize programmatically; on Polkadot TestNet, use the [Bulletin Chain authorization page](https://paritytech.github.io/polkadot-bulletin-chain/authorizations) before submitting any `store` extrinsic from your Product.

`CloudStorageClient` (introduced in [Store a Larger File](#store-a-larger-file)) exposes `client.checkAuthorization(address)` as a pre-flight check before submitting a store:

```typescript title="check-authorization.ts"
// Place this in your Product, after the setup from `setup-client.ts`.

import { client, account } from './setup-client';

const auth = await client.checkAuthorization(account.address);

if (!auth.authorized)`);
} else {
  console.log(`Remaining transactions: ${auth.remainingTransactions}`);
  console.log(`Remaining bytes:        ${auth.remainingBytes}`);
  console.log(`Expires at block:       ${auth.expiration}`);
}

// Estimate authorization needed for a hypothetical 2 MiB payload.
const estimate = client.estimateAuthorization(2 * 1024 * 1024);
console.log(`To store 2 MiB you need ~${estimate.transactions} txs, ${estimate.bytes} bytes`);
```

The returned `AuthorizationStatus`:

- **`authorized`**: `true` when an authorization record exists for the account.
- **`remainingTransactions`**: Number of `store` calls remaining in the quota.
- **`remainingBytes`**: Bytes remaining across those calls (`bigint`).
- **`expiration`**: The block at which any unused quota expires.

`client.estimateAuthorization(dataSize)` returns the `{ transactions, bytes }` you would need to authorize a hypothetical payload of `dataSize` bytes, which is useful before requesting a quota top-up.


Renew Long-Lived Data

## Renew Long-Lived Data

Stored data is retained for roughly two weeks. If your Product needs the data to outlive that window, renew the storage record before it expires.

Renewal needs the `(block, index)` pair from the `Stored` event of the original write. That is where `app.cloudStorage.upload()` runs out of road. `upload()` returns only the CID string. To get the bookkeeping pair, use `CloudStorageClient.store(...).send()` instead, which returns a full `StoreResult` (`cid`, `blockNumber`, `extrinsicIndex`, `size`). Persist `(blockNumber, extrinsicIndex)` for each record you intend to renew.

As you approach the expiry block (current block + retention period), submit a renewal:

```typescript title="renew-data.ts"
// Place this in your Product, after the setup from `setup-client.ts`.

import { client } from './setup-client';

// Captured from the Stored event of the original store; persist these in
// your Product. Each renewal returns a NEW (block, index) — track the
// latest values, reusing the original ones on a future renewal will fail.
declare const lastBlock: number;
declare const lastIndex: number;

const receipt = await client.renew(lastBlock, lastIndex).send();

console.log(`Renewed in block:  ${receipt.blockHash}`);
console.log(`Tx hash:           ${receipt.txHash}`);
console.log(`Block number:      ${receipt.blockNumber}`);
```

`client.renew(block, index).send()` builds and submits the call, returning a `TransactionReceipt` with `blockHash`, `txHash`, and `blockNumber`. The `Renewed` event carries the new `(block, index)` pair; capture it so the next renewal uses the latest values.

The SDK's typed Bulletin API does not expose `RetentionPeriod` or the `Utility` pallet directly. If you need to read the retention period from chain state, or batch many renewals atomically via `Utility.batch_all`, generate the full PAPI bulletin descriptor with `npx papi add bulletin -w <RPC>` and submit through `polkadot-api` directly because `@parity/product-sdk-cloud-storage` is intentionally a narrow surface.

!!! warning
    Each renewal generates a new `(block, index)` pair. Track the values from the latest `Renewed` event for any subsequent renewal. Using the original values after a renewal will fail. The Bulletin Chain pallet does not emit retention events ahead of expiry; your Product needs its own scheduler (cron job, queue, or background worker) to renew before the storage expires.


Cross-Chain Storage from People Chain

## Cross-Chain Storage from People Chain

PoP-gated identity lives on the People Chain. When a Product needs to attach content to that identity (for example, a PoP-Lite communication identifier or a verified-person attestation), the store has to be initiated on People Chain and dispatched to the Bulletin Chain via XCM.

!!! warning "Provisional"
    The cross-chain path is in flight. The flow described here is the intended shape; XCM message format and authorization model may change before the path is finalized.

The flow has three phases:

1. People Chain authorizes your account against its local `transactionStorage` instance (authorization on People Chain is independent of your Bulletin Chain authorization).
2. Your account submits `transactionStorage.store(data)` on People Chain. The receipt yields a People-Chain-side `(block, index)` pair plus the computed CID.
3. People Chain dispatches an XCM message to the Bulletin Chain that mirrors the storage record. Once XCM execution completes, the data is addressable from the Bulletin Chain's collator network with the same CID. Your Product can read it via `client.fetchBytes(cid)` exactly as if you had written directly to Bulletin.

Until the XCM dispatch is wired up, treat this section as the design contract for the path; the hand-rolled cross-chain code samples will be added once the pallet shape stabilizes.


Submit a Preimage

## Submit a Preimage

Bulletin Chain has a second authorization model alongside the per-account quota you've been using. Instead of authorizing your account to store transactions and bytes, a privileged caller (Root on Bulletin, or the People Chain via the cross-chain dispatch covered in [Cross-Chain Storage from People Chain](#cross-chain-storage-from-people-chain)) can pre-authorize a specific content hash via the `authorize_preimage` extrinsic. Once that authorization is in place, anyone (including your Product) can submit the matching bytes via an unsigned transaction: no fees, no per-account quota debited.

This is the right path when:

- A sponsor (an app, a parachain, or governance) pre-authorizes content for someone else to upload.
- The People Chain → Bulletin XCM flow described in [Cross-Chain Storage from People Chain](#cross-chain-storage-from-people-chain) authorizes a hash on Bulletin, and the actual bytes get submitted by the user's Product.

The Host API exposes the submission side through `getPreimageManager` from `@parity/product-sdk-host` (already installed via the umbrella package in Set Up). Polkadot Desktop mediates the call. The Product never holds a signer for this path because the underlying transaction is unsigned.

```typescript title="submit-preimage.ts"
import { getPreimageManager } from '@parity/product-sdk-host';

const preimageManager = await getPreimageManager();
if (!preimageManager)

const payload = new TextEncoder().encode('preimage payload bytes');

const key = await preimageManager.submit(payload);

console.log(`Preimage submitted. Key (resource lookup): ${key}`);
```

`preimageManager.submit(payload)` resolves with the Blake2b-256 hash of the payload, which is the same hash format as a Bulletin CID. The submission is rejected if no `authorize_preimage` exists for that hash. Reading is permissionless; subscribe via `preimageManager.lookup(key, callback)`.

!!! warning "Provisional"
    The Bulletin Chain preimage authorization flow is live on TestNet today, but the cross-chain authorization path (People Chain → Bulletin) and production environment endpoints are not yet finalized. The submission has a Host-side timeout (~120s on the current dev build) before it resolves; production timeouts may shift. The `@parity/product-sdk` API surface is pre-1.0; minor API changes are expected during the `0.x` line.

The mechanics:

- Some upstream caller (Root on Bulletin, or People Chain via XCM) calls `authorize_preimage(contentHash, maxSize)`.
- Your Product calls `preimageManager.submit(payload)`.
- Polkadot Desktop computes the Blake2b-256 hash of the payload; the chain accepts the submission only if a matching authorization exists.
- The bytes are stored on Bulletin Chain via an unsigned transaction with no fees and no per-account quota debited.
- Reading is permissionless: any account can fetch the bytes by hash via `preimageManager.lookup`.
- Retention is roughly two weeks per the standard Bulletin Chain retention window; renewal works the same way as for account-authorized stores.
- Per-transaction byte limit is the same ~8 MiB; larger payloads are split into chunks and authorized as a DAG-PB manifest plus the chunk hashes.

For the underlying pallet surface (`authorize_preimage`, `refresh_preimage_authorization`, `remove_expired_preimage_authorization`), see [Preimage Authorization](/reference/polkadot-hub/data-storage/#preimage-authorization) in the Data Storage reference.


Storage Paths at a Glance

## Storage Paths at a Glance

The flows in this guide target the same chain but differ in authorization, atomicity, and consumer access. Use this table to pick the right path before writing.

|              Path               |                    Authorization                     |          Atomicity           |      Retention       |                            Use When                             |
|:-------------------------------:|:----------------------------------------------------:|:----------------------------:|:--------------------:|:---------------------------------------------------------------:|
| Bulletin store (small)          | Bulletin authorization                               | Single tx                    | ~2 weeks (renewable) | Most Product writes                                             |
| Bulletin store (chunked)        | Bulletin authorization                               | Multi-tx + DAG-PB manifest   | ~2 weeks (renewable) | Files larger than 8 MiB                                         |
| Cross-chain via People Chain    | People-Chain authorization                           | XCM (eventually consistent)  | ~2 weeks (renewable) | PoP-attached writes                                             |
| Bulletin preimage submission    | Pre-authorized hash (no per-account quota, no fees)  | Single unsigned tx           | ~2 weeks (renewable) | Sponsored uploads; receiving People Chain → Bulletin XCM dispatches |

For deeper comparison and the full pallet reference, see [Data Storage Reference](/reference/polkadot-hub/data-storage/).


Where to Go Next

## Where to Go Next

-   Guide **Publish and Subscribe to Off-Chain Data**

    ---

    Your Product can store durable content; next, add real-time state between users via the Statement Store.

    [:octicons-arrow-right-24: Publish and Subscribe to Off-Chain Data](/apps/build/pub-sub-off-chain-data/)

-   Guide **Read On-Chain Data**

    ---

    Pair Bulletin writes with chain reads via the Host API's PAPI provider.

    [:octicons-arrow-right-24: Read On-Chain Data](/apps/build/read-chain-state/)

-   External **Product SDK API Reference**

    ---

    The full `product-sdk` surface beyond this recipe: every package, class, and method.

    [:octicons-arrow-right-24: Visit Site](https://paritytech.github.io/product-sdk/)


Introduction

## Introduction

This page covers how to build and deploy a Polkadot Product using the `playground` CLI. By the end, your app bundle will be uploaded to the Bulletin Chain, registered under a `.dot` name, and discoverable in the Polkadot playground.


Prerequisites

## Prerequisites

Before deploying, ensure you have:

- Complete [Install Desktop and Pair](/apps/get-started/) and [Get TestNet Tokens](/apps/get-started/get-testnet-tokens/); your account needs PAS funds and a Bulletin Chain authorization. If you have not obtained a Bulletin Chain authorization yet, request one from the [Bulletin Chain authorization page](https://paritytech.github.io/polkadot-bulletin-chain/authorizations)
- A Polkadot Product project running locally. See [Set Up Your Project](/apps/build/#set-up-your-project)


Build Your App Bundle

## Build Your App Bundle

Run `playground build` to compile your project into a deployable bundle.

  playground build

The CLI auto-detects your project type and runs the appropriate build. The output is a set of static files (HTML, JS, CSS, assets) that will be uploaded in the next step.

!!! tip
    If you have already built the project, you can skip this step. `playground deploy` will prompt you to reuse the existing build.


Deploy Your App

## Deploy Your App

Run `playground deploy` to start the interactive deploy flow. The CLI walks you through a series of prompts, then shows a confirmation summary before uploading.

  playground deploy

The CLI presents the following prompts in order:

1. **Review app detail page**: A reminder that your `README.md` becomes your app's detail page on the playground. Make sure it's up to date, then press ++enter++ to continue (or ++esc++ to exit and edit it first).

2. **Redeploy contracts if changed**: Smart contracts hold your app's on-chain logic and data, and deploy separately from your website. Choose **no** if you only changed the website. Choose **yes** if you changed contract code in this project. The CLI then redeploys and reinstalls the contracts and rebuilds the site to match.

  did you change your smart contracts?
      › no  ·  I only changed the website
        yes  ·  I changed contract code too

3. **Choose to rebuild before deployment**: Compiles your latest code into the files that get uploaded. Choose **yes** to rebuild now, or **no** to redeploy the build that's already in your build folder.

  build before deploy?
      › yes  ·  rebuild with my latest code
        no  ·  redeploy the existing build

4. **Choose who signs the upload**: Publishing writes to the blockchain, which needs a signature. The **dev signer** uses a shared test account: instant, no phone needed. The **phone signer** signs with your own logged-in account, with a few taps on your phone.

  who signs the upload?
      › dev signer  ·  fast, no phone needed
        your phone signer  ·  signs with your own account

5. **Choose a default build directory**:  The folder holding your built site (the files that get uploaded). The default `dist` fits most projects. This example uses `.next` for a Next.js app.

  build directory  default: dist
      › .next█

6. **Choose a domain name**: Pick the `.dot` address people will use to reach your app, e.g. `my-app.dot`. Name availability depends on the length of the base name (the part before any optional two-digit suffix):

    | Base name length       | Requirement                                         |
    |------------------------|-----------------------------------------------------|
    | 9 characters or longer | Open to everyone — deploys with no personhood check |
    | 6 to 8 characters      | Requires Proof of Personhood on this network        |
    | 5 characters or fewer  | Reserved                                            |

  domain
      › myproject57█

7. **Publish to the playground**: Choose **yes** to list your app in the public Polkadot Playground so others can find and open it. Choose **no** to still deploy it to your `.dot` address, but keep it unlisted.

  publish to the playground?
      › yes  ·  list it in the public playground
        no  ·  deploy to my .dot address only

8. **Review confirmation summary**: The CLI shows a summary of your choices before uploading. Review it, then press ++enter++ to deploy (or ++esc++ to cancel). With the dev signer, no phone taps are needed and `phone approvals` reads `none`.

  playground deploy  ·  myproject57.dot  ·  paseo next v2        v0.34.7
      ────────────────────────────────────────────────────────────────────────

      deploying myproject57.dot

      signer        Dev signer (no phone taps for upload)
      build         skip (use existing)
      build dir     .next
      contracts     skip
      publish       DotNS only

      phone approvals none

      enter to deploy  ·  esc to cancel

    Press ++enter++ to confirm. The CLI then runs the upload and on-chain registration steps. If you chose the **phone signer**, each step triggers an approval prompt in the Polkadot mobile app — open the app and approve when prompted. With the **dev signer** selected here, the upload and DotNS registration run automatically with no phone prompts, and the deploy finalizes:

  playground deploy  ·  myproject57.dot  ·  paseo next v2        v0.34.7
      ────────────────────────────────────────────────────────────────────────

      frontend

      · build         skipped
      ✓ upload + dotns

      ✓ deploy complete

      url           https://myproject57.dot.li
      domain        myproject57.dot
      app cid       bafybeihvru3e6ojhopxj7xxwtafrpyvsha6kylzklryon5k67u4clr26re
      ipfs cid      bafybeigr2liqwftbmily4sdxvo7mq4atgboqsrpdadypmsbpkn7c25cwja


Open Your App

## Open Your App

Once the deploy completes, the CLI prints the URLs for your app. Regardless of whether you published it to the playground, your app is live at its `.dot` address and reachable through the `.dot.li` gateway:

```
https://myproject57.dot.li
```

You can also navigate directly by entering your `.dot` name in the Polkadot Desktop browser address bar:

```
myproject57.dot
```

![Successfully deployed app reachable at its .dot URL](/images/apps/deploy-your-app/deploy-your-app-01.webp)

Either way, the app loads directly from the Bulletin Chain — no central server involved.

If you chose **yes** at the `publish to the playground?` prompt, your app is also listed in the public playground directory. Open `playground.dot` in Polkadot Desktop browser and your app appears under your `.dot` name, so others can find and open it. If you chose **no** (`DotNS only`, as in this example), the app is still fully deployed and reachable at the URLs above — it just won't be listed in the directory.

!!! tip
    If your app does not appear immediately, wait a few seconds and refresh. On-chain state propagation can take a short time after the deploy transaction finalizes.


Prerequisites

## Prerequisites

Before getting started, ensure you have:

- Completed the [Install Polkadot Desktop and Pair](/apps/get-started/) guide so Polkadot Desktop is paired with your signer


Get Tokens

## Get Tokens

The Polkadot Faucet distributes free PAS tokens to developers.

1. Open the [Polkadot Faucet](https://faucet.polkadot.io/) and select your target network from the **Network** drop-down.

    ![Polkadot TestNet Faucet showing the Network and Chain dropdowns, the Paseo Address input, the reCAPTCHA check, and the Get some PASs button.](/images/apps/get-started/get-testnet-tokens/get-testnet-tokens-01.webp)

2. Paste the address of the account paired with Polkadot Desktop into the address field.
3. Click **Get Some PASs** to request tokens. They arrive in your account shortly after the request is processed.

!!! warning "Provisional"
    The faucet's mapping to the current Paseo Next v2 chains is being confirmed. If your funds do not appear in Polkadot Desktop after a few minutes, contact the developer community for the current chain selection.


Service Allowances

## Service Allowances

Some Polkadot infrastructure services use a separate allowance-based access model. These allowances are independent of your token balance; even with enough PAS to cover fees, a missing allowance will cause the service to reject your request.

??? note "Service Allowances: Bulletin Chain Storage"

    The [Bulletin Chain](/reference/apps/infrastructure/bulletin-chain/) has no token balance for storage. Every account needs an explicit authorization that grants a quota of transactions and bytes before it can store data on-chain. Without authorization, storage extrinsics are rejected.

    On TestNet, request your storage authorization directly from the [Bulletin Chain authorization page](https://paritytech.github.io/polkadot-bulletin-chain/authorizations). This is required before submitting any `store` extrinsic from your Product.

    After your authorization request is confirmed, you can verify the allocation on-chain by querying the `Authorizations` storage map of the `transaction-storage` pallet for your account.

??? note "Service Allowances: Statement Store"

    The [Statement Store](/reference/apps/infrastructure/statement-store/) lets accounts publish off-chain statements that are gossiped and persisted by the network. Access is controlled by an on-chain `StatementAllowance` record that specifies two limits per account: `max_count` (the maximum number of statements the account can publish) and `max_size` (the maximum total bytes across those statements).

    !!! warning "Provisional"
        The process for obtaining a Statement Store allowance on TestNet is not yet documented. Check back for updates, or ask in the developer community for available access paths.

??? note "Service Allowances: dotNS Names"

    [dotNS](/reference/apps/infrastructure/dotns/) (`.dot` name registration) uses a hybrid model. Open names (those that anyone can register) require a deposit paid in PAS. Names reserved for accounts with Proof of Personhood Full or PoP Lite status are free to register for eligible accounts, with no deposit required.

    See the [dotNS PopRules pricing reference](/reference/apps/infrastructure/dotns/poprules-pricing/) for full pricing and tier details.


Where to Go Next

## Where to Go Next

-   Guide **Build Your Product**

    ---

    Set up a local project, load it in Polkadot Desktop via the `localhost` bypass, and add capabilities guide by guide.

    [:octicons-arrow-right-24: Set Up Your Project](/apps/build/#set-up-your-project)


Introduction

## Introduction

You should already have the Polkadot App on your phone from the [Apps overview](/apps/); it holds your key and approves signing. This page covers installing Polkadot Desktop, where your Product runs, and pairing the two with a QR scan, then forwards you to TestNet funding. About 10 minutes.

Two pieces of the Polkadot Triangle need to be talking to each other: Polkadot Desktop, where your Product runs, and the Polkadot App on your phone, where signing happens.

![Diagram: Polkadot Desktop displays a QR code that the Polkadot App scans; the App returns a session public key, and from then on every signing request routes from Desktop to the App for signature.](/images/apps/get-started/install-and-pair/pairing-flow.svg)

Polkadot Desktop never holds your private key. Your identity lives on the Polkadot [People Chain](/reference/glossary/#people-chain), your private key lives in the Polkadot App, and Polkadot Desktop only ever holds a derived session public key — enough to identify you and construct per-Product accounts, but not enough to sign anything on its own.


Prerequisites

## Prerequisites

Before getting started, ensure you have:

- The [Polkadot App](/apps/) installed on your phone with an account created (your developer identity and signing device for Polkadot Products)
- A workstation running macOS, Windows, or Linux
- A device (iOS or Android) with a working camera
- Network connectivity on both devices


Install Polkadot Desktop

## Install Polkadot Desktop

1. Download the development build of [Polkadot Desktop](https://www.polkadotcommunity.foundation/desktop).

2. Install the application using your platform's standard installer.

3. Launch Polkadot Desktop. On first launch, Desktop opens into the pairing flow with a QR code.

    ![Polkadot Desktop login screen showing the pairing QR code and the network selector at the top right with Paseo Next V2 selected.](/images/apps/get-started/install-and-pair/install-and-pair-02.webp)

    !!! note "Skip for development"
        The login screen also exposes a **Skip (Dev only)** button. Skipping the pairing drops you straight into Desktop without a paired signer, useful for inspecting Desktop or testing a local Product that does not require signing, but most development flows assume a paired Polkadot App.


## Pair Polkadot Desktop with the Polkadot App

Pairing is a one-time cryptographic handshake. Desktop displays the QR code, the App scans it, and the App returns a session public key that Desktop stores. From that point forward, Desktop knows who you are and can construct per-Product sub-accounts, but every signing prompt still routes back to the App for approval.

1. Leave the QR code visible on the Polkadot Desktop login screen.

2. In the Polkadot App, open the camera-scanning view and scan the QR code shown on Desktop. A **Link a new device?** prompt appears with the Desktop version details.

    ![Polkadot App showing the "Link a new device?" prompt listing Polkadot Desktop with Cancel and Link buttons.](/images/apps/get-started/install-and-pair/install-and-pair-03.webp)

3. Tap **Link** to confirm. The App briefly shows a **Connecting device...** state while the handshake completes.

    ![Polkadot App showing the "Connecting device..." state with a loading spinner.](/images/apps/get-started/install-and-pair/install-and-pair-04.webp)

4. Desktop transitions from the QR code to a **Completing pairing...** state.

    ![Polkadot Desktop showing the "Completing pairing..." state with a loading spinner where the QR code was.](/images/apps/get-started/install-and-pair/install-and-pair-05.webp)

5. Once the handshake completes, Desktop opens the main dashboard.

    ![Polkadot Desktop showing the paired dashboard with the address bar and Browse section listing available Products.](/images/apps/get-started/install-and-pair/install-and-pair-06.webp)

After pairing, your identity on the People Chain is bound to the Polkadot App for this Desktop session. Every subsequent signing request will route to the App, and you approve or reject each one on the signing device.


Where to Go Next

## Where to Go Next

-   Guide **Get TestNet Tokens**

    ---

    Claim TestNet tokens from the Polkadot Faucet and unlock per-service allowances.

    [:octicons-arrow-right-24: Continue](/apps/get-started/get-testnet-tokens/)


Get the Polkadot App

## Get the Polkadot App

The Polkadot App is your wallet, identity, and signer: the center of everything you build. Install it first; every path below connects through it.

<div class="button-wrapper" markdown>
[:material-apple: App Store — Coming soon]()   [:material-google-play: Google Play](https://play.google.com/store/apps/details?id=io.pcf.polkadotapp)
</div>


Then Pick Your Path

## Then Pick Your Path

-   Guide **Deploy a Product in Under 15 Minutes**

    ---

    Go from nothing to a live `.dot` Product: in your browser with RevX, or from the terminal with the CLI. No local environment to set up.

    [:octicons-arrow-right-24: Quick Start](/apps/quick-start/)

-   Guide **Develop Locally (the Full Route)**

    ---

    Install Polkadot Desktop, pair it with your phone, and get TestNet tokens, then build your Product capability by capability.

    [:octicons-arrow-right-24: Get Started](/apps/get-started/)


What Is a Polkadot Product?

## What Is a Polkadot Product?

_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 (e.g., `awesome.dot`), 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.

_Polkadot Apps_ are the three applications that can host Polkadot Products, collectively known as the _Polkadot Triangle_:

- **[Polkadot Desktop](/reference/apps/hosts/polkadot-desktop/)**: The workstation host. Loads Polkadot Products by their `.dot` name and runs them in a sandbox.
- **[Polkadot App](/reference/apps/hosts/polkadot-app/)**: The mobile wallet and signer. Holds the user's private key and approves every signing request.
- **[Polkadot Web](/reference/apps/hosts/polkadot-web/)**: The browser host at `dot.li`. Resolves `.dot` names client-side via a light client and renders the Product in a sandboxed iframe.

You build Polkadot Products. They run inside one of the Polkadot Apps. This section teaches you how.

![The Polkadot Triangle: Polkadot Desktop at the top, Polkadot App at the bottom-left, Polkadot Web at the bottom-right, with Polkadot Product running inside](/images/apps/index/polkadot-triangle.svg)

For the architectural breakdown covering how the Product, SDK, Host, and Polkadot infrastructure relate, see the [App Development Reference](/reference/apps/).


Why Build a Polkadot Product?

## Why Build a Polkadot Product?

Polkadot Apps is a complete environment for building Web3 decentralized applications:

**Wallet built-in**: The Polkadot App on the user's phone is the wallet for every Polkadot Product they use. No wallet integration code, no "Connect Wallet" button to design, no signing plumbing to maintain. Your Product receives a derived per-user account and asks for signatures; approvals happen on the user's phone.

**Identity built-in**: Per-Product accounts are derived from the user's `.dot` identity, so there is no signup flow. With [Proof of Personhood](/reference/apps/infrastructure/pop/), you can gate features on verified-human status without seeing who the user is, a privacy-preserving humans-only access model built into the platform. Need to recognize the same user across two of your Products? Same alias system, scoped to your Product set.

**Decentralized hosting**: Your bundle lives on a decentralized cloud storage provider. Your `.dot` name lives in a decentralized name service. There is no centralized hosting provider, no DNS service, no platform in the middle. Users fetch your Product directly and verify it themselves.

**Product SDK**: The Product SDK covers what you would otherwise integrate yourself:

- **Chain access**: Query state and submit transactions through the Host. No RPC servers to operate; signing prompts open on the user's phone.
- **Decentralized storage**: Upload files, get a permanent content-addressed URL.
- **Real-time signed messaging**: Pub/sub between users of your Product via the [Statement Store](/reference/apps/infrastructure/statement-store/), every message verifiable.
- **Privacy-preserving payments**: Request, top up, track status. Built on [Coinage](/reference/apps/infrastructure/pop/pallet-coinage/).
- **Chat**: Rooms, bots, interactive action buttons.
- **Identity**: Derived per-Product accounts, plus optional [Proof of Personhood](/reference/apps/infrastructure/pop/) gating.
- **Local storage**: Per-Product key/value, persisted on the user's device.

Permissions (microphone access, outbound network requests, on-chain transaction submission, and more) are declared in your Product's [manifest](/reference/apps/hosts/polkadot-desktop/permissions/) and prompted at runtime. Users see exactly what your Product can access before they grant it. The Host enforces the boundary inside its [sandbox](/reference/apps/protocol/truapi/sandbox/), not your code.

**Three Hosts, one Product**: The same `.dot` bundle runs in the mobile Polkadot App, on the workstation in Polkadot Desktop, and in any browser via Polkadot Web. You do not write three apps; the Triangle abstracts the platform.


Prerequisites

## Prerequisites

Before starting, make sure you have:

- The [Polkadot App](/apps/) installed on your phone with an account created; it pairs with `pg` and signs the publish step.
- A terminal with `curl` available and permission to install CLI tools in your user shell environment.
- A Polkadot Product project on disk with a package-manager build command.

!!! note "CLI version"
    This route targets playground-cli `0.27.1`. The CLI is in active development and breaking changes between versions are expected. To follow along, install this version, or check this page's last update against the latest release.


Install the CLI

## Install the CLI

1. Run the installer:

    ```bash
    curl -fsSL https://raw.githubusercontent.com/paritytech/playground-cli/main/install.sh | bash
    ```

2. Open a new shell, or `source` your RC file, and verify the install:

    ```bash
    pg --version
    ```

!!! note "Command aliases"
    The installer registers two interchangeable commands: `playground` (canonical) and `pg` (short alias). This guide uses `pg` throughout.


Initialize

## Initialize

`pg init` installs toolchain dependencies and pairs the CLI with your signer. It is safe to re-run; existing installs and sessions are detected and skipped.

```bash
pg init
```

    pg init

    ✔ logged in         5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
    ✔ username          alice.dot
    ✔ product account   5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty (0x90b5ab205c6974c9ea841be688864633dc9ca8a3)
    ✔ Setup complete

!!! tip
    Toolchain install runs in parallel with the login step, so you can scan the QR code while dependencies download.

!!! note "Signer modes"
    - **Mobile signer** (`--signer phone`, default): Pairs with the Polkadot App via QR code. Required for `pg init` and recommended for any deploy you intend to keep.
    - **Dev-only signer** (`--signer dev`): No phone needed; uses shared development keys (for example, `--suri //Alice`). The deployed `.dot` Product will be owned by the shared dev account, not by you.


Build

## Build

`pg build` auto-detects and runs your project build.

```bash
pg build
```

    pg build

    ✔ Build succeeded → dist/


Deploy

## Deploy

`pg deploy` runs the full pipeline: build the frontend, upload artifacts to the Polkadot Bulletin Chain, and register a `.dot` domain via DotNS. Before building, it always runs your package manager's install step to keep dependencies in sync.

```bash
# Interactive - pg prompts for signer, domain, and build directory
pg deploy

# Dev signer - no phone needed (the deployed Product is owned by the shared dev account)
pg deploy --signer dev --domain my-app
```

    pg deploy --signer dev --domain my-app

    ✔ my-app.dot is available
    ✔ Deploy complete
      URL         https://dot.li/...
      Domain      my-app.dot
      App CID     Qm...

!!! note
    `pg deploy` includes a memory watchdog that aborts the deploy if the process exceeds 4 GB RSS. If you hit this limit, set `DOT_MEMORY_TRACE=1` alongside `DOT_DEPLOY_VERBOSE=1` to capture per-second RSS and heap samples.

??? note "More CLI commands"

    - **`pg mod`**: Clones a moddable app from the Playground registry so you can customize and redeploy it as your own Product. Only apps that opted into `--moddable` at deploy time are listed. Pass a domain label, such as `my-app` or `my-app.dot`, to clone directly, or omit it to open an interactive picker showing every moddable app.

        ```bash
        pg mod [domain]
        ```

    - **`pg logout`**: Signs out of the paired account and clears session files under `~/.polkadot-apps/`. A no-op if you are not signed in.

        ```bash
        pg logout
        ```

    - **`pg update`**: Updates `pg` to the latest version from the GitHub releases page.

        ```bash
        pg update
        ```

You have deployed a Polkadot Product. To keep building it with your own editor and toolchain, head to the Build guides; they open with [project setup](/apps/build/#set-up-your-project) so Polkadot Desktop can load your Product from `localhost` while you iterate with live reload.


Where to Go Next

## Where to Go Next

-   Guide **Build Guides**

    ---

    Set up the local dev loop, then add capabilities to your Product: signing, on-chain reads, decentralized storage, off-chain pub/sub, and local persistence.

    [:octicons-arrow-right-24: Open Build Guides](/apps/build/)


Prerequisites

## Prerequisites

Before starting, make sure you have:

- The [Polkadot App](/apps/) installed on your phone with an account created; it signs the final publish step.
- An API key for an AI provider supported by App Builder, such as OpenAI or Anthropic.


Open RevX

## Open RevX

Open [RevX](https://revx.dev/) in your browser. RevX is a browser-based IDE; you will use the App Builder track to scaffold a Polkadot Product end to end.

![RevX IDE landing view showing the main workspace](/images/apps/quick-start/revx/revx-01.webp)


Open App Builder

## Open App Builder

From the sidebar, select **App Builder**. App Builder is RevX's track for generating Polkadot Products using AI-assisted workflows.

![RevX sidebar with the App Builder option highlighted](/images/apps/quick-start/revx/revx-02.webp)


Create a New App

## Create a New App

Click **Create App** to start a new project. RevX automatically installs the required dependencies and initializes the project environment so you can move straight into configuration and prompting.

![RevX App Builder showing the Create App action and the initialized project environment](/images/apps/quick-start/revx/revx-03.webp)

After clicking **Create App**, you will see a loading screen while the IDE initializes the project environment.

![RevX App Builder showing the loading screen while the IDE initializes the project environment](/images/apps/quick-start/revx/revx-04.webp)

!!! note
    Initial setup may take a moment while dependencies install. Wait for the environment to finish initializing before configuring the builder.

Once the environment is initialized, you will see a minimal app scaffolded in the editor and running in the terminal.

![RevX App Builder showing the minimal app scaffolded in the editor and running in the terminal](/images/apps/quick-start/revx/revx-05.webp)


Configure the Builder

## Configure the Builder

From the App Builder view, click the **Settings** icon to open the AI configuration panel.

![RevX App Builder settings icon](/images/apps/quick-start/revx/revx-06.webp)

Configure the following:

- **AI provider**: The backend service that powers code generation.
- **API key**: The credential for your AI provider.
- **Model**: The specific model to use from the selected provider.

![RevX App Builder configuration panel showing API key, AI provider, and model fields](/images/apps/quick-start/revx/revx-07.webp)

These settings let the IDE generate applications using the selected AI backend. You can change them at any time if you want to switch providers or models.