sealed.vote is a browser-based 1-10 score voting application built around threshold-elgamal. It uses a public roster, an append-only bulletin-board-style log, and local verification so that voters can audit who is participating while keeping ballot contents confidential.
sealed-vote-demo.mp4
(recorded with Playwright, I got bored of manually re-recording the demo video after UI changes quite quickly)
apps/webReact and Vite frontendapps/apiFastify API backed by PostgreSQLpackages/contractsshared request and response contractspackages/protocolshared poll phase, tallying, and crypto helperspackages/testkitshared backend and e2e test helperstests/e2ePlaywright browser tests
The frontend and backend both rely on threshold-elgamal, a TypeScript cryptography library used for the board ceremony, threshold encryption workflow, and local verification.
Closed polls publish a frozen manifest with rosterHash, optionList, and the fixed score range { min: 1, max: 10 }, and every signed board payload is versioned with protocolVersion: 'v1'.
- A poll creator opens a score poll and shares its slug-based URL.
- Voters join the waiting room with public names and receive voter-specific tokens.
- Once at least three voters are registered, the creator starts voting and the roster becomes fixed.
- The client signs and appends protocol payloads to the board log behind guided UI actions. The board is append-only and every message is classified as accepted, idempotent, or equivocation.
- The public read model derives ceremony phase, digests, manifest state, and verification status only from the ordered board entries.
- After voting closes, the app completes the DKG, encrypted ballot publication, ballot-close, decryption-share, and tally-publication flow automatically in the browser, then verifies the final result from the public board log.
This repository currently targets a hardened research prototype, not audited production voting software.
See docs/voting.md for the board ceremony model, and docs/endpoints.md for the current API surface.
- Frontend: TypeScript, React, Redux Toolkit, Tailwind CSS, shadcn/ui, Vite, Vitest
- Backend: TypeScript, Fastify, Drizzle ORM, PostgreSQL, Vitest
- Tooling: pnpm workspaces, Turborepo, Playwright, ESLint, stylelint (web app)
Offline and reconnect recovery is a core feature of the app, not a best-effort extra.
- The browser persists only narrow local session state: creator tokens, voter tokens, voter indices, and poll references needed to reconnect to the same ceremony.
- On reopen, the app refetches the public read model and board log from the API instead of restoring cached poll snapshots from a service worker.
- Board message retransmissions are safe because the backend classifies identical unsigned payloads as idempotent, even when the signatures differ.
- Plaintext scores are intentionally persisted in local browser storage so the same device can finish the post-close ceremony after a refresh or reconnect. They are cleared only after that local vote can no longer participate in the active ceremony.
- Node.js
>=24.14.1 pnpm@10.33.0- Docker Desktop or another Docker engine with Compose support
From the repository root:
pnpm install
pnpm local:reset
pnpm devpnpm local:reset recreates the Docker services, resets the database, and seeds local sample data in one step.
The default local setup serves:
- the web app at
http://127.0.0.1:3000 - the API at
http://127.0.0.1:4000
- Run the standard local browser suite with
pnpm e2e. - Run the opt-in high-parallelism local suite with
pnpm e2e:turbo. - Run production-only repros in the prepared Linux Playwright container with
pnpm e2e:debug:production -- tests/e2e/ceremony-persistence.spec.ts --project firefox-desktop. - Use
pnpm e2e:debug:shellfor an interactive debug shell in the prepared container. - See tests/e2e/debugging/README.md for the full production-debug workflow and artifact locations.
- Run
pnpm demo:recordto generate a stitched three-panel README demo video intest-results/readme-demo/sealed-vote-demo.mp4. The command also writes the raw per-panel videos and manifest intest-results/readme-demo/and requires localffmpeg. pnpm e2e:turbokeeps CI unchanged, enables PlaywrightfullyParallel, and defaults to up to 8 local workers.- Set
PLAYWRIGHT_LOCAL_WORKERSif you want a different default for turbo mode. Passing Playwright CLI flags such as--workers=32still overrides the config directly.
- apps/api/README.md for API workspace
- apps/web/README.md for frontend workspace
- docs/endpoints.md for endpoint documentation
- docs/voting.md for the voting protocol
This repository is licensed under AGPL-3.0-only. See LICENSE for the full text.