Skip to content

Tenemo/sealed-vote

Repository files navigation

sealed.vote

Netlify Status API status


Production E2E tests CI Tests coverage


Node version License


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)

Overview

  • apps/web React and Vite frontend
  • apps/api Fastify API backed by PostgreSQL
  • packages/contracts shared request and response contracts
  • packages/protocol shared poll phase, tallying, and crypto helpers
  • packages/testkit shared backend and e2e test helpers
  • tests/e2e Playwright 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'.

How it works

  1. A poll creator opens a score poll and shares its slug-based URL.
  2. Voters join the waiting room with public names and receive voter-specific tokens.
  3. Once at least three voters are registered, the creator starts voting and the roster becomes fixed.
  4. 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.
  5. The public read model derives ceremony phase, digests, manifest state, and verification status only from the ordered board entries.
  6. 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.

Tech stack

  • 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

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.

Local development

Requirements

  • Node.js >=24.14.1
  • pnpm@10.33.0
  • Docker Desktop or another Docker engine with Compose support

Running the full stack

From the repository root:

pnpm install
pnpm local:reset
pnpm dev

pnpm 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

Running tests locally

  • 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:shell for 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:record to generate a stitched three-panel README demo video in test-results/readme-demo/sealed-vote-demo.mp4. The command also writes the raw per-panel videos and manifest in test-results/readme-demo/ and requires local ffmpeg.
  • pnpm e2e:turbo keeps CI unchanged, enables Playwright fullyParallel, and defaults to up to 8 local workers.
  • Set PLAYWRIGHT_LOCAL_WORKERS if you want a different default for turbo mode. Passing Playwright CLI flags such as --workers=32 still overrides the config directly.

Workspace documentation

License

This repository is licensed under AGPL-3.0-only. See LICENSE for the full text.

About

A browser-based application relying on homomorphic encryption allowing for 1-10 score voting in a way such that only the result is publicly known - the votes aren't revealed to neither the server, nor other voters. Uses the threshold-elgamal package: https://www.npmjs.com/package/threshold-elgamal

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages