Local Development
This guide covers advanced local development topics. For basic setup, see the Quick Start.
Starting the Local Bee Cluster
Section titled “Starting the Local Bee Cluster”The project uses @snaha/bee-compose to run a local Bee cluster with blockchain support.
Quick Start
Section titled “Quick Start”# Start everything: cluster and chain in background, identity site and demo in foregroundpnpm dev:local
# The same, from a clean chain and empty node statepnpm dev:local:fresh
# Tear the containers downpnpm dev:local:stopRe-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.
All Cluster Commands
Section titled “All Cluster Commands”| 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.
Local Endpoints
Section titled “Local Endpoints”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.
Tunnels
Section titled “Tunnels”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.
Developer Tools (/dev)
Section titled “Developer Tools (/dev)”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.
Overview Tab
Section titled “Overview Tab”- 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
Chain Tab
Section titled “Chain Tab”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.
Node Tab
Section titled “Node Tab”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.
Devices Tab
Section titled “Devices Tab”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.
Getting a Postage Batch
Section titled “Getting a Postage Batch”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.
Using Developer Tools (Recommended)
Section titled “Using Developer Tools (Recommended)”- Navigate to
http://localhost:5500/dev - Go to the Chain tab and select an account
- 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.
Using the API
Section titled “Using the API”# Buy a stamp with amount 500000000 and depth 20curl -X POST "http://localhost:1633/stamps/500000000/20"Understanding Stamp Parameters
Section titled “Understanding Stamp Parameters”| 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.
Paying for a Drive
Section titled “Paying for a Drive”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:
- Start everything with
pnpm dev:local. - Point the app at the local endpoints: the header menu on
/devswitches 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. - 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. - 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.
Common Problems
Section titled “Common Problems”Stamp Not Usable After Purchase
Section titled “Stamp Not Usable After Purchase”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 Storage Partitioning
Section titled “Safari Storage Partitioning”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.
Storage Partitioning Debugging
Section titled “Storage Partitioning Debugging”To check if storage is partitioned:
- Open browser DevTools
- Go to Application → Storage
- 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.
Bee Requests Blocked by CORS
Section titled “Bee Requests Blocked by CORS”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.
Bee Cluster Won’t Start
Section titled “Bee Cluster Won’t Start”- Ensure Docker is running
- Check if ports 1633, 16331, or 9545 are already in use
- Try
pnpm dev:cluster:start --freshfor a clean start - After a
@snaha/bee-composeupgrade, add--pullso the chain image is rebuilt rather than reused
Library Changes Not Reflected
Section titled “Library Changes Not Reflected”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.
Development Tips
Section titled “Development Tips”- TypeScript Execution: Use
pnpx tsxinstead ofnpx ts-nodeto 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
Client-Side Stamp Signing
Section titled “Client-Side Stamp Signing”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)Known Dev Keys
Section titled “Known Dev Keys”These are the pre-funded keys available in the local Bee cluster:
| Node | Private Key | Address |
|---|---|---|
| Queen | 566058308ad5fa3888173c741a1fb902c9f1f19559b11fc2738dfc53637ce4e9 |
0x26234a2ad3ba8b398a762f279b792cfacd536a3f |
| Worker 1 | 195cf6324303f6941ad119d0a1d2e862d810078e1370b8d205552a543ff40aab |
- |
Testing with Real Domains (SSH Tunnel)
Section titled “Testing with Real Domains (SSH Tunnel)”To test storage partitioning behavior with real TLS certificates (as in production), you can use SSH tunnels to a VPS with nginx.
Architecture
Section titled “Architecture”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) │└─────────────────────────┘ └─────────────────────┘VPS Setup (one-time)
Section titled “VPS Setup (one-time)”- Add nginx server blocks pointing to
127.0.0.1:18080(demo) and127.0.0.1:5500(identity) - Get SSL certificates with certbot
- Add DNS A records for both subdomains
Local Usage
Section titled “Local Usage”# Terminal 1: Start demo serverpnpm dev:demo
# Terminal 2: Start the SvelteKit dev serverpnpm dev:ui
# Terminal 3: Open SSH tunnelssh -R 18080:localhost:3500 -R 5500:localhost:5500 user@your-vpsAccess the Demo
Section titled “Access the Demo”https://demo.yourdomain.com/?idDomain=https://id.yourdomain.comThe ?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.