Skip to content

Local Development

This guide covers advanced local development topics. For basic setup, see the Quick Start.

The project uses @snaha/bee-compose to run a local Bee cluster with blockchain support.

Terminal window
# Start everything: cluster and chain in background, identity site and demo in foreground
pnpm dev:local
# The same, from a clean chain and empty node state
pnpm dev:local:fresh
# Tear the containers down
pnpm dev:local:stop

Re-running pnpm dev:local is a no-op for whatever is already up. To drive the pieces separately, pnpm dev:cluster:start starts just the containers and pnpm dev just the servers.

Command Description
pnpm dev:cluster:start Start cluster in background (queen + 3)
pnpm dev:cluster:start --fresh Clean start (destroys node state)
pnpm dev:cluster stop Stop the cluster
pnpm dev:cluster status Show what is running
pnpm dev:cluster:logs Follow queen node logs

pnpm dev:cluster passes anything through to the bee-compose CLI, so pnpm dev:cluster --help lists the rest.

Once the cluster is running, these endpoints are available:

Service URL
Queen Bee API http://localhost:1633
Worker 1 API http://localhost:16331
Blockchain RPC (fake Gnosis) http://localhost:9545
Fake source chain (pnpm dev:local) http://localhost:31337
Account bus signaling ws://localhost:5520

pnpm dev:local also starts a fake source chain (an Anvil instance with chain id 31337) and the local solver that stands in for Relay, so a payment from another chain can be rehearsed end to end; see Paying for a Drive.

Every node in the cluster allows all CORS origins, so the identity site can talk to http://localhost:1633 as soon as it is up. A Bee node you run yourself instead needs the identity UI’s origin allowed explicitly — see Using Your Own Bee Node.

Behind a tunnel (ngrok, Cloudflare Tunnel), the app’s origin is the tunnel hostname, not localhost: the identity site sees a different app and derives a different app secret, so a session approved on localhost does not carry over. Some dev servers also refuse cross-origin requests from anything but localhost — Next.js needs the tunnel host in allowedDevOrigins — and the symptom is the proxy iframe rejecting every message rather than anything about the tunnel.

The Identity site includes a Developer Tools page at http://localhost:5500/dev with utilities for testing and debugging.

A menu in the page header switches the whole app between the local endpoints and the production ones in a click; anything else is edited through the product’s own Network settings dialog. Below it, a banner names the chain the configured RPC actually serves — proven by its genesis hash, since the local chain reports Gnosis’ chain id on purpose — so it is always visible whether the tools below would be spending real money.

  • Endpoints these tools use — the Bee node, the cluster worker (a second node, for checking that a chunk replicated off the node that took it), the Gnosis Chain RPC, and the fake Ethereum mainnet RPC, each with a live indicator
  • Test connect flow — a link to the demo app on port 3500 to run the connect flow against this identity site
  • Local data — counts of the accounts, connections, and stamps on this device, with Clear accounts and Clear everything buttons

Everything that talks to the Gnosis-side chain, acting on the account selected at the top of the page. These tools refuse to run anywhere but a chain that is provably a development one.

  • Buying a drive — Add drive offers two payment methods. The built-in engine — which the paid drive operations (extend, resize) also use — buys for real, against whichever chain the app is pointed at. Locally that is the baked dev chain, so the same code path as mainnet runs on a chain whose funds are worth nothing. See Paying for a Drive. The other method, the widget, settles on mainnet only; locally the simulated purchase below stands in for it.
  • Simulated purchase — makes the widget method of Add drive settle a mocked batch instead of a real cross-chain payment. That widget only settles on mainnet, so this is what makes that method reachable locally at all. The batch it leaves behind is fabricated, which is why extend and resize cannot act on it. Nothing here touches the built-in method.
  • Wallet networks — adds the chains a payment can be signed on to MetaMask, so a balance shows before you reach the payment screens. Gnosis Chain (fake) — the local chain at http://localhost:9545 — is the one to add.
  • Faucet — hands any address xDAI, BZZ, WXDAI or USDC out of the float the local chain is baked with, and shows what that address and the faucet hold. The two ERC20s are the ones a drive can be paid in, so a token payment is testable without trading for the token first.
  • On-chain drive tooling — buys a batch the account’s own postage signer OWNS, running the real purchase steps (swap → approve → createBatch) against the local chain’s BZZ pool. Create drive to test with goes one step further and attaches the batch as an ordinary drive; that is the button to reach for before hand-testing extend or resize. A companion makes one that is already inside the “Expires soon” window, for testing that warning.
  • Import batch by ID — reads a batch’s parameters straight from the PostageStamp contract, so any batch id works even when the configured node has never seen it. The signer key is not on chain, so paste the one the batch was bought with.

Everything that talks to the Bee node:

  • Stored stamps — the batches saved in this browser, with the fields needed to re-adopt one on a fresh account.
  • Retrievability self-check — writes a throwaway single-owner chunk and reads it back, to confirm the node serves back what you write. Multi-device coordination (and reliable sync) only works where it does; a companion read-by-address runs the same check across two devices, which is the one that mirrors real coordination on a load-balanced gateway.
  • Manual sync testing — triggers a sync for every account, to exercise postage-stamp utilization tracking. Detailed [StateSync] logs go to the browser console.
  • Partition tuning — overrides the multi-device intent-round timings for a gateway with slower propagation.

The devices registered to the selected account, which partitions each currently holds, and an Online badge for every device heard on the account bus in the last three minutes. The rows refresh every 5 s, so a peer shows up about one presence message (20 s) after it joins.

Postage stamps are required for uploading data to Swarm. In development, you can get stamps through the Developer Tools, via API, or through the product’s own Add drive flow, paying from a wallet — see Paying for a Drive.

  1. Navigate to http://localhost:5500/dev
  2. Go to the Chain tab and select an account
  3. Click Create drive to test with

That buys a real batch on the local chain, owned by the account’s own postage signer, and attaches it as a drive — with no signer keys to copy between fields. To adopt a batch bought elsewhere instead, use Import batch by ID on the same tab.

Terminal window
# Buy a stamp with amount 500000000 and depth 20
curl -X POST "http://localhost:1633/stamps/500000000/20"
Parameter Description
Amount How much BZZ to allocate (affects storage duration)
Depth Determines how much data can be stored (2^depth chunks)

For local development, the defaults (amount: 500000000, depth: 20) are sufficient for testing. The amount must exceed roughly 414720000 — the local chain’s postage price (24000) multiplied by Bee’s 17280-block (~24h) minimum validity period. Smaller amounts are rejected with an HTTP 400 error.

Buying, extending and resizing a drive all cost money, and the money is real even locally: the payment is a transfer from your wallet to the batch owner on Gnosis — in xDAI, WXDAI, USDC or BZZ — which is then swapped for BZZ through a real SushiSwap pool and spent against the real PostageStamp contract. Locally all of that runs against the baked hybrid chain at the production addresses, so it is the same code path as mainnet on a chain whose funds are worth nothing.

Where the chain carries the EIP-7702 delegate, as Gnosis mainnet does, the postage calls run as one atomic transaction; the baked snapshot cannot carry it, so locally they run one at a time until Create drive to test with splices it in.

That is the payment dialog’s built-in method. The other one, the widget — the method offered first when buying a drive — settles on Gnosis mainnet only and has nothing to settle against here, so either pick the built-in method or turn on the Simulated purchase toggles described above.

To run the flow end to end:

  1. Start everything with pnpm dev:local.
  2. Point the app at the local endpoints: the header menu on /dev switches Bee node and RPC to Use local in one click. Anything else is edited through the Network settings dialog, also reachable from the product’s Settings menu.
  3. On /dev → Chain, use Wallet networks to add Gnosis Chain (fake) (http://localhost:9545, chain id 100) to MetaMask, then use the Faucet to stock the account you will connect with — xDAI, WXDAI, USDC or BZZ, the assets a drive can be paid in. Nothing funds the payer for you: the wallet must already hold what it pays with.
  4. Add or manage a drive in the product UI and pay in the dialog — for a new drive, switch the method to Pay with crypto (built in, experimental) first — then connect the wallet, pick a token, and sign.

Chain id alone cannot tell the local chain from the real one — it answers as 100 deliberately — so before anything is signed the app compares genesis hashes, which a chain cannot borrow, and refuses in words when your wallet is on real Gnosis while the app is pointed at the local chain (or the reverse). That is what stops a rehearsal spending real xDAI.

Reach for pnpm dev:local:fresh when the chain has drifted — every purchase trades against a real, thin BZZ pool, and a fresh start restores the baked snapshot. It also wipes node state, so drives created earlier will point at batches that no longer exist; clear the UI’s site data too.

Paying from a chain other than Gnosis goes through Relay, which only exists on mainnet. Locally the fake source chain and the local solver started by pnpm dev:local stand in for it: the wallet deposits on the fake source chain, and the solver delivers to the batch owner on the local Gnosis chain out of its faucet. See docs/Drive-Payment-Flow.md for the full design.

After buying a postage stamp, wait ~30 seconds before using it. The stamp needs to be registered on the blockchain before the Bee node accepts it.

Safari’s Intelligent Tracking Prevention (ITP) partitions storage for third-party iframes, so the proxy cannot read the trusted domain’s localStorage. Uploads are designed to keep working — the connect popup hands the iframe the account’s stamps and signer keys — and the session is kept in that partition’s own store, so a reload does not re-run the handshake (#635). See issue #277 for the background.

The whole path — handover, stamper, upload — is confirmed on real Safari (iOS 18.7 / Safari 26.6), in a normal and a private window alike. A plain pnpm dev cannot reproduce it — the identity site and demo are served from the same site, and ports do not make them cross-site. Browsing the demo under a different hostname for the same machine does: http://127.0.0.1:3500 or http://[::1]:3500 while the proxy origin stays http://localhost:5500. That is the rig ui/tests/bus-propagation.test.ts uses.

Locally, pnpm dev also starts the account-bus signaling server on port 5520; without it a partitioned iframe can only reach contexts in its own partition.

Safari private browsing sessions are also ephemeral — data is lost when the window closes.

To check if storage is partitioned:

  1. Open browser DevTools
  2. Go to Application → Storage
  3. Look for partitioned storage indicators

On the local rig storage is not partitioned at all: localhost:3500 and localhost:5500 are the same site (ports do not affect site), so the iframe reads the identity site’s first-party store directly. Browsing the demo under a different name for the same machine — 127.0.0.1:3500 instead of localhost:3500 — makes it cross-site and partitions it, which is the quickest way to reproduce the partitioned path locally.

The cluster started by pnpm dev:cluster:start allows every origin, so this only bites a Bee node you run by hand: it has to allow the identity site’s origin (http://localhost:5500 in development), because Bee requests are made by the Swarm ID iframe rather than by the app embedding it. See Using Your Own Bee Node.

  • Ensure Docker is running
  • Check if ports 1633, 16331, or 9545 are already in use
  • Try pnpm dev:cluster:start --fresh for a clean start
  • After a @snaha/bee-compose upgrade, add --pull so the chain image is rebuilt rather than reused

The identity site and the demo dev servers import the library’s source directly, so an edit in lib/src shows up on the next reload without a build. Two things still need a restart of pnpm dev: switching branches, and running pnpm build:lib. Either can leave the dev server’s module graph stale, which shows up as a missing export that plainly exists in the file.

A build (pnpm build:lib) is only needed for the built demo and for anything that consumes the published package.

  • TypeScript Execution: Use pnpx tsx instead of npx ts-node to run TypeScript files
  • Hot Reload: Changes in ui/src/ will automatically reload in the browser
  • Debugging: Use browser DevTools on both the parent page and the iframe

A drive created from the /dev Chain tab is owned by the account’s derived postage signer — copy its key from Stored stamps on the Node tab. A batch bought by hand against a Bee node’s own API is owned by that node instead (the queen key below). Either way, client-side signing uses the owning key:

import { Stamper } from '@ethersphere/bee-js'
const stamper = Stamper.fromBlank(signerKey, batchId, depth)
const envelope = stamper.stamp(chunk)

These are the pre-funded keys available in the local Bee cluster:

Node Private Key Address
Queen 566058308ad5fa3888173c741a1fb902c9f1f19559b11fc2738dfc53637ce4e9 0x26234a2ad3ba8b398a762f279b792cfacd536a3f
Worker 1 195cf6324303f6941ad119d0a1d2e862d810078e1370b8d205552a543ff40aab -

To test storage partitioning behavior with real TLS certificates (as in production), you can use SSH tunnels to a VPS with nginx.

Your VPS (nginx + HTTPS) Your Local Machine
┌─────────────────────────┐ ┌─────────────────────┐
│ demo.yourdomain.com │◄──────────│ SSH -R tunnels │
│ → 127.0.0.1:18080 │ │ 18080 → demo │
│ id.yourdomain.com │ │ 5500 → identity │
│ → 127.0.0.1:5500 │ │ (Vite dev server) │
└─────────────────────────┘ └─────────────────────┘
  1. Add nginx server blocks pointing to 127.0.0.1:18080 (demo) and 127.0.0.1:5500 (identity)
  2. Get SSL certificates with certbot
  3. Add DNS A records for both subdomains
Terminal window
# Terminal 1: Start demo server
pnpm dev:demo
# Terminal 2: Start the SvelteKit dev server
pnpm dev:ui
# Terminal 3: Open SSH tunnel
ssh -R 18080:localhost:3500 -R 5500:localhost:5500 user@your-vps
https://demo.yourdomain.com/?idDomain=https://id.yourdomain.com

The ?idDomain= parameter tells the demo which identity service to use. This allows testing cross-origin storage partitioning with real browser security policies while still having hot reload for the identity site.