React frontend for Dotizen. Single-page app using HashRouter (works from any static host) with WebAuthn passkey sign-in and client-side Groth16 ZK proof generation.
- React 18 + Vite 6 + TypeScript (strict)
- Tailwind CSS with a Material Design 3 token palette
- Polkadot-API (PAPI) for chain interaction
- Zustand for state management
@polkadot-labs/hdkd+@scure/bip39for sr25519 derivation and mnemonics- WebAuthn + PRF extension for passkey-based sign-in (no browser extension needed)
- WASM ZK module at
src/lib/zk/for Groth16 proof generation
See spec/web.md for the full design.
| Route | Purpose |
|---|---|
/signin |
Sign in / register / recover (three tabs); dev-mode quick login as Alice/Bob/Charlie |
/ |
Dashboard: onboarding status, active proposals, voting summary |
/proposals |
List with filter (all / active / passed / failed) |
/proposal/:id |
Description, image, tally, vote buttons, comments |
/proposals/new |
Create proposal: title, description, image, scope, funding source, deadline |
/membership |
Committee view: pending applications, members, elections, Q&A, key rotation |
/my-home |
Home info, linked devices, pairing QR, voting-key registration |
/pair, /pair/:seed/:label/:payload |
Process pairing payload from a scanned QR |
/statements |
Browse on-chain binary statements (text, images, PDFs, WASM) |
All routes except /signin and /pair* redirect to /signin if no currentAccount is in the store.
Frontend only (chain must already be running):
cd web
npm install
npm run devOr from the repo root, scripted (sets the right VITE_LOCAL_WS_URL for the active local stack):
./scripts/start-frontend.shFull stack in one command:
./scripts/start-all.shcp .env.example .env.local| Variable | When to set | Notes |
|---|---|---|
VITE_LOCAL_WS_URL |
Local dev with custom port offset | The local-stack scripts export this automatically |
VITE_WS_URL |
Hosted builds | Build-time default for the WebSocket endpoint |
VITE_PINATA_API_KEY |
Hosting proposal images / rich descriptions | Without it, the proposal form falls back to text-only |
VITE_PINATA_SECRET_KEY |
As above | Pair with VITE_PINATA_API_KEY |
Generated descriptors live in .papi/ and are committed so a fresh checkout builds without a running chain. Regenerate against a running chain after pallet changes:
npm run update-types # fetch fresh metadata
npm run codegen # regenerate descriptorsThe repo fails fast if papi generate errors, which makes descriptor drift easier to spot.
npm run dev # Vite dev server (HTTPS on 5173 by default)
npm run build # production build to dist/
npm run lint # ESLint
npm run fmt # Prettier
npm run fmt:check # Prettier check-onlyThree-tab flow on /signin:
- Register — creates a passkey via WebAuthn, generates a 12-word BIP39 mnemonic for the identity key, asks the user to confirm the first/last words, then submits the unsigned
Membership.applyextrinsic with PII encrypted via X25519 + HKDF + AES-GCM. - Sign in — WebAuthn
get()with the device-PRF salt re-derives the symmetric key and decrypts the device seed. - Recover — restores from the BIP39 mnemonic; dev-account mnemonics short-circuit straight into the corresponding dev session.
Keys held client-side: device seed (sr25519, per device, PRF-encrypted), home secret (per home, distributed via pairing QR), identity key (origin device only, PRF-encrypted). See src/lib/auth/passkey.ts and the Authentication section of spec/web.md for details.
Groth16 proofs are generated entirely in-browser via the WASM module in src/lib/zk/. The proving key blob lives at public/zk-proving-key.bin (large; cached after first use). cast_vote is unsigned — the proof is the authorisation, with a small client-side proof-of-work (blakejs) replacing gas. See spec/voting.md for the protocol.