This guide covers deploying the frontend and parachain runtime.
The frontend is a static Vite app that works on any hosting platform. It uses hash-based routing (HashRouter) and relative asset paths (base: "./") so it works correctly on IPFS gateways, GitHub Pages, and subdirectory deployments without configuration.
The app exposes the Substrate WebSocket endpoint on the home page. On localhost it defaults to the local dev URL; on hosted deployments it defaults to Polkadot Hub TestNet. You can also set a build-time default with VITE_WS_URL (see web/.env.example).
The simplest option for public demos.
Setup (one-time):
- Go to your repo Settings > Pages
- Under Source, select GitHub Actions
How it works:
The workflow at .github/workflows/deploy-github-pages.yml runs automatically on push to main/master. It builds the frontend and deploys to GitHub Pages.
Your site will be available at:
https://<username>.github.io/<repo-name>/
Manual trigger:
Go to Actions > Deploy to GitHub Pages > Run workflow to trigger a deploy without pushing code.
Deploys the frontend to IPFS and registers a .dot domain that resolves to it via the Polkadot naming system.
How it works:
The workflow at .github/workflows/deploy-frontend.yml is manual on purpose. It:
- Builds the frontend
- Uploads to IPFS
- Registers/updates the DotNS domain via
paritytech/dotns-sdk
The domain basename is entered when you dispatch the workflow. Domain registration is automatic (register-base: true).
Configuration:
- Open Actions > Deploy Frontend to DotNS > Run workflow
- Enter a unique DotNS basename (lowercase, 9+ letters followed by exactly 2 digits, e.g.
my-cool-project42) - The workflow uses Alice's dev account by default, which works for free registration on Paseo testnet. To use your own account, set the
DOTNS_MNEMONICsecret in your repo settings.
Local IPFS deployment:
You can also deploy to IPFS locally without CI:
# Install web3.storage CLI (one-time)
npm install -g @web3-storage/w3cli
w3 login your@email.com
w3 space create polkadot-stack-template
# Deploy
./scripts/deploy-frontend.shThis builds the frontend, uploads to IPFS, and prints the gateway URL plus the DotNS follow-up steps.
Since the frontend is a static build, it works on any static hosting:
cd web && npm install && npm run build
# Output: web/dist/Upload web/dist/ to Vercel, Netlify, Cloudflare Pages, S3, or any static file server.
# Build and start with polkadot-omni-node
./scripts/start-dev.shThis builds the runtime WASM, generates a chain spec, and starts the lightweight solo-node path. Endpoints:
- Substrate RPC:
ws://127.0.0.1:9944by default
This solo-node mode is intentionally optimised for quick runtime and pallet iteration. Use the relay-backed scripts (./scripts/start-all.sh or ./scripts/start-local.sh) when you want the full Polkadot stack.
All local scripts also support STACK_PORT_OFFSET plus explicit STACK_SUBSTRATE_RPC_PORT and STACK_FRONTEND_PORT overrides. When you use those scripts, the frontend dev server, CLI defaults, and PAPI refresh all follow the active port settings automatically.
The local scripts currently start omni-node with the equivalent of:
polkadot-omni-node \
--chain blockchain/chain_spec.json \
--tmp \
--alice \
--force-authoring \
--dev-block-time 3000 \
--unsafe-force-node-key-generation \
--rpc-cors allWhat each flag is doing:
--chain blockchain/chain_spec.json: run this template's generated chain spec instead of omni-node's built-indevchain--tmp: use a temporary base path and delete chain data on shutdown--alice: use Alice's dev keys for authoring and signing--force-authoring: keep producing blocks even without peers--dev-block-time 3000: use omni-node's solo dev sealing mode so blocks keep authoring without a relay chain--unsafe-force-node-key-generation: allow omni-node to generate a temporary network key for this throwaway local authority--rpc-cors all: keep browser-based local tooling working without extra CORS setup
When you might change these later:
- Remove
--tmpif you want local chain state to persist across restarts. - If you remove
--tmp, also set an explicit--base-pathso you control where chain data is stored. - If you remove
--tmp, you should also stop relying on--unsafe-force-node-key-generationand generate a stable node key instead. - Replace
--alicewith another dev account or your own key setup if you do not want Alice authoring blocks. - Remove
--force-authoringif you only want block production when the node is fully participating in a network. - Remove
--dev-block-timeonly if you are switching to a relay-backed environment such as Zombienet.
This repo now generates a repo-specific chain ID instead of the generic custom default. That reduces accidental collisions with other local projects. If you move to a persistent base path later, it is still a good idea to keep the base path unique per project.
./scripts/start-local.shUse ./scripts/start-all.sh if you want the relay-backed network plus frontend startup in one command.
If you need a second relay-backed stack at the same time:
STACK_PORT_OFFSET=100 ./scripts/start-local.shProposal descriptions and per-proposal photos are stored off-chain on IPFS via Pinata; only the CID is recorded on-chain.
Genesis fixtures are pinned by scripts/pin-genesis-content.mjs:
VITE_PINATA_API_KEY=... VITE_PINATA_SECRET_KEY=... \
node scripts/pin-genesis-content.mjsThis pins each entry in blockchain/genesis-content/proposals.json, the per-slug images in blockchain/genesis-content/images/, and re-encrypts the residents' phone numbers in blockchain/genesis-content/homes.json with the dev committee public key. The output goes to blockchain/runtime/src/genesis_content.rs; commit the result. Pass --skip-ipfs to refresh phone ciphertexts only.
Frontend uploads (proposal images and rich descriptions) use the same Pinata account; set VITE_PINATA_API_KEY and VITE_PINATA_SECRET_KEY in web/.env (see web/.env.example). Without credentials the proposal form falls back to text-only.
All write commands accept --signer (-s) which auto-detects the format:
--signer alice # dev account name
--signer "bottom drive obey lake ..." # mnemonic phrase
--signer 0x5fb92d6e98884f76de468fa3f... # raw secret seedDefault is alice if omitted.
# Chain info
cargo run -p stack-cli -- chain info
cargo run -p stack-cli -- chain blocks
# Membership
cargo run -p stack-cli -- membership apply --flat-number 305 --block-id 1 --signer //newuser
cargo run -p stack-cli -- membership approve <application-hash> --signer alice
cargo run -p stack-cli -- membership list-homes
cargo run -p stack-cli -- membership show-committee
cargo run -p stack-cli -- membership start-election --signer alice
# Governance
# create-proposal is an unsigned bare call (CheckCreateProposal extension);
# --signer only identifies the author. --cost is required unless --funding none.
cargo run -p stack-cli -- governance create-proposal --title "Install EV chargers" \
--description-cid QmXyz... --deadline 1776902400000 \
--funding reserve --cost 45000 --signer alice
cargo run -p stack-cli -- governance list-proposals
cargo run -p stack-cli -- governance get-tally <proposal-id>
cargo run -p stack-cli -- governance close-proposal <proposal-id> --signer aliceSee cargo run -p stack-cli -- membership --help and governance --help for the full subcommand list (elections, comments, transfer, candidate Q&A, etc.).
Use --url to target a different endpoint:
cargo run -p stack-cli -- --url wss://your-node:9944 governance list-proposals