π Language: English (canonical) Β· EspaΓ±ol
ElectΓ³metro is a digital Voting Advice Application (VAA) β an interactive questionnaire that helps voters discover which parties and presidential candidates best match their political positions. It is inspired by Germany's Wahl-o-Mat model (Bundeszentrale fΓΌr politische Bildung, bpb), adapted to the political, institutional, and social context of Latin American elections.
The goal is simple: empower citizens with clear, accessible information so they can make an informed vote, make political platforms more visible, and strengthen the legitimacy of the democratic process.
Live deployments: electometro.org Β·
- How it works
- Features
- Screenshots
- Tech stack
- Installation & development
- Build
- Deployment
- Project structure
- Configuration
- FAQ
- Contributing
- License
- Voters answer a questionnaire of theses (β10β20) about key national debate topics.
- For each thesis the voter chooses agree / neutral / disagree, and can additionally mark whole topics as very important (which boosts the weight of related questions).
- Based on the answers and importance weighting, the app computes the voter's programmatic and ideological closeness to each party and presidential candidate (a weighted similarity score).
- Detailed per-question breakdowns show how each entity voted on every thesis.
Results can be encoded into a shareable mnemonic phrase (a sequence of words in the URL ?r=
parameter) so a voter can save or share their result without an account.
- Responsive design (mobile and desktop) with a draggable widget-based layout (
react-grid-layout). - Smooth navigation across unique questions.
- Topic-importance weighting for questions the voter cares about.
- Optional demographic form (gender, age, education, region).
- Expanded candidate/party detail views with per-question comparison.
- Legacy-browser support (Chrome 70+, Safari 12+, iOS 12+) via
@vitejs/plugin-legacy.
- Weighted similarity scoring against parties and presidential candidates.
- Results partitioned by a minimum number of compared questions (
MIN_COMPARED = 8). - Random tie-breaking among equal scores to avoid ordering bias.
- Per-question match visualization.
- Multi-round support (e.g. first / second round) with per-round candidate and party filtering.
- Multi-language support via Tolgee.
- Dynamic language switching without page reload.
- Languages currently wired: Spanish (
es), Quechua (qu), Aymara (ay).
- CAPTCHA verification before submission (Cloudflare Turnstile, with an hCaptcha fallback for older browsers that cannot run Turnstile).
- Device fingerprinting via
fpscanner/ FingerprintJS. - Hidden honeypot field to trap bots.
- Database-side validation and Row-Level Security (see
db/).
- Consent-gated usage analytics via Trench.js.
- Quiz-completion and demographic metrics, only when the user consents.
| Intro | Quiz | Results |
|---|---|---|
![]() |
![]() |
![]() |
- βοΈ React 18 with Vite 6 (build + dev server)
- π React Router 7 (
HashRouter) - π§© react-grid-layout for the draggable widget canvas
- π£οΈ Tolgee for internationalization
- βοΈ Cloudflare Workers (edge compute) β the API/Worker lives in a separate repository, included
here as the
external/cf-workersgit submodule. The exact API, asset, and data contracts are documented in the Submodules discussion. - ποΈ Supabase (PostgreSQL) for response storage
- π¦ Cloudflare R2 for translation assets
- π Cloudflare KV for fingerprint β cookie binding
Vercel might be used on a fallback scenario
- π Cloudflare Turnstile (+ hCaptcha fallback)
- ποΈ FingerprintJS /
fpscanner - π‘οΈ Supabase Row-Level Security + validation triggers
- π Trench.js (consent-gated analytics)
Note on dependencies: Some dependencies are not used by the frontend SPA in this repository; they relate to the standalone Worker/API. See the Architecture discussion for dependency boundaries.
- Node.js >= 18 and
npm - A Cloudflare account (Workers, KV, R2) for the tracked deployment path
- A Supabase account
- (Optional) A Tolgee account to manage translations
-
Clone the repository (with submodules if you have access to the asset/worker repos):
git clone --recurse-submodules https://github.com/electometro-org/app.git electometro-app cd electometro-app -
Install dependencies:
npm install
-
Configure environment variables. Copy the example files and fill in the values:
cp .env.example .env cp .env.development.example .env.development cp .env.local.example .env.local
See Configuration for what each variable does.
-
Provide the
public/andi18n/assets. The app needs apublic/directory with the election assets and anindex.html, plus ani18n/directory of translations. Neither is committed to this repo. Either create them manually following the structure below, or symlink the assets submodule:git submodule add <assets-repo-url> external/peru-assets ln -s ./external/peru-assets/app/public ./public ln -s ./external/peru-assets/app/i18n ./i18n
Expected layout:
public/ βββ index.html # main page (use index.html.example as a template) βββ static/ # static assets (copied to dist/ on build) βββ favicon.svg βββ i18n/ # translation files (optional) βββ {election_id}/ # election-specific assets (e.g. peru_2026/) βββ combined_votes_*.json # generated vote data (optional in dev)index.html.exampleat the repo root is a template β copy it topublic/index.htmland adjust the CSP and meta tags for your domain. -
Provide the
wrangler/config (Cloudflare target only). Also not committed:mkdir -p wrangler cp wrangler.toml.example wrangler/wrangler.toml # then edit values # β¦or, with submodule access: ln -s ./external/cf-workers/peru_2026/wrangler ./wrangler
See the Wrangler configuration docs.
-
Run the database migrations in the Supabase SQL editor, in order:
db/migration.sqlβ createsquiz_answers, indexes, RLS.db/security.sqlβ check constraints + validation triggers.
-
Start the dev server:
npm run dev
| Script | Purpose |
|---|---|
npm run dev |
Vite dev server (--mode development) |
npm run build |
Production build |
npm run build-dev |
Development-mode build |
npm run preview |
Build + serve via wrangler dev |
npm run preview-qa |
QA preview with CLOUDFLARE_ENV=qa |
npm run deploy |
Build + wrangler deploy |
npm run deploy-qa |
QA deploy with CLOUDFLARE_ENV=qa |
npm run lint |
Run ESLint over the project |
The current tracked build is Cloudflare-oriented and resolved in vite.config.js.
It uses the Cloudflare Vite plugin, public/index.html as the entry file, public/static as the
public asset directory, legacy browser output, and the nest-assets-plugin, which moves built assets
under /electometro/ to work around Wrangler asset-resolution limitations.
A per-election build also injects HTML <title>, description, canonical URL, favicon, and lang via
vite-plugin-election-html.js, keyed on VITE_ELECTION_ID.
- Set secrets:
wrangler secret put TURNSTILE_SECRET_KEY wrangler secret put VITE_SUPABASE_PUBLISHABLE_KEY
- Deploy:
npm run deploy - Configure a custom domain in the Cloudflare dashboard (Pages β Custom domains).
- Create the R2 bucket for translations (Tolgee keeps it updated):
wrangler r2 bucket create electometro-i18n
A GitHub Actions workflow (.github/workflows/cloudflare-healthcheck.yml) mentions considering a DNS
migration to Vercel if Cloudflare is degraded. Vercel deployment files and npm scripts are not tracked
in this checkout, so Cloudflare is the documented deployment path here.
electometro/
βββ db/ # Supabase SQL: schema (migration.sql) + hardening (security.sql)
βββ docs/ # Versioned project documentation
β βββ DECISIONS/ # Architecture Decision Records (ADRs)
β βββ es/ # Spanish translations of community-facing docs
β βββ TEST_SUMMARY.md # Test suite coverage summary
β βββ assets/ # Documentation images
βββ src/
β βββ App.jsx # Root component, routes, view switching
β βββ main.jsx # Vite entry point
β βββ backgrounds/ # Background system (registry + types: solid/image/slideshow/gradient)
β βββ components/ # Reusable UI components (modals, CTAs, forms, menu, static pages)
β βββ config/ # appConfig, branding, env, background defaults
β βββ constants/ # answerMappings, capibarismoMapping
β βββ contexts/ # Split state providers (Election/QuizFlow/Results/UI + QuizContext)
β βββ elections/ # Per-election config registry (peru_2026, chile_2025) + widgets
β βββ hooks/ # useQuiz reducer + focused quiz hooks (flow, results, submission, β¦)
β βββ services/ # quizService, resultsService, submissionService (logic layer)
β βββ utils/ # mnemonicCodec, versionUtils
β βββ views/ # Top-level screens (intro, selector, quiz, results, etc.)
β βββ widgets/ # Widget system (registry + docking + types)
βββ vite-plugin-election-html.js # Injects per-election meta tags at build
βββ vite.config.js # Cloudflare-oriented Vite build config
βββ wrangler.toml.example # Template for Cloudflare deployment config
The naming and folder conventions are documented in the Conventions discussion.
Environment variables are read by Vite (import.meta.env). Copy from .env.example and fill in:
| Variable | Purpose |
|---|---|
VITE_TRENCH_ENABLED |
Enable/disable Trench analytics. |
VITE_TRENCH_SERVER_URL / VITE_TRENCH_PUBLIC_API_KEY |
Trench analytics endpoint + key. |
VITE_TURNSTILE_FORM_KEY |
Cloudflare Turnstile site key. |
VITE_HCAPTCHA_SITE_KEY |
hCaptcha site key (fallback for old browsers). |
VITE_HCAPTCHA_FALLBACK_API |
Fallback API base URL for hCaptcha submissions. |
VITE_I18N_URL |
Base URL for translation assets (R2). |
VITE_ELECTIONS_DATA_URL |
Base URL for election vote data (compact JSON). |
VITE_ELECTION_ID |
Which election(s) are enabled. Single ID = single-election mode (skips selector); comma-separated = multi-election; empty = all elections with enabled: true. |
VITE_USE_ELECTION_BRANDING |
false keeps neutral branding for the whole session. |
VITE_SHOW_GENERIC_INTRO |
Toggle the neutral intro shown before election selection. |
VITE_SHOW_ELECTION_INTRO |
Toggle the election-specific intro before the quiz. |
VITE_TOLGEE_QA_TRANSLATIONS |
Use the /qa data/translation prefix. |
Application flow flags are resolved in src/config/appConfig.js; branding
in src/config/branding.js. Adding a new election is a matter of dropping a
config file into src/elections/ and registering it in
src/elections/index.js β see the Architecture discussion.
Why HashRouter instead of BrowserRouter?
The app is served as a static SPA, sometimes mounted under a sub-path (/electometro) of a host
microfrontend. Hash routing avoids server rewrite requirements for deep links.
Do I need a backend to run it locally?
For the quiz and results you only need the election vote data (VITE_ELECTIONS_DATA_URL). Submitting
answers requires the Worker/API + Supabase; without them, submissions simply fail silently and the
quiz still works.
Where does the vote data come from?
From external compact-format JSON files (keys like t1, c1, p1) served from
VITE_ELECTIONS_DATA_URL. The frontend does not generate this data.
Can I add a new election or country? Yes β that's a core design goal. Add an election config and assets; see the Architecture discussion and CONTRIBUTING.md.
Are there automated tests?
Yes β a Vitest suite under tests/ covers the pure logic modules (services/, utils/,
constants/); run it with npm test. See ROADMAP for the longer-term plan (views and hooks are not yet covered).
Contributions are welcome β this project aims to strengthen democracy through open technology. Please read:
- CONTRIBUTING.md β setup, branch/commit conventions, PR & review process
- CODE_OF_CONDUCT.md β community standards
- SECURITY.md β reporting vulnerabilities
- AGENTS.md β condensed, agent-facing guidance for AI coding tools
- π Docs discussions β reference documentation: Architecture, Conventions, Submodules (asset & Worker/API contracts), Elections System, Widget System, Background System, and the Development Guide
Architecture decisions (ADRs) are versioned in docs/DECISIONS/. The roadmap lives in ROADMAP.
Electometro is licensed under the Apache License 2.0. The package metadata also declares
"license": "Apache-2.0".
Apache-2.0 is a permissive OSI-approved license with an explicit patent grant, attribution/notice rules, and trademark boundaries that fit a public-interest civic technology project.
- Bundeszentrale fΓΌr politische Bildung (bpb) for the Wahl-o-Mat model.
- The open-source community for the tools used here.
- Contributors and the voters who use the tool to make informed decisions.
Made with β€οΈ to strengthen democracy in Latin America.


