Who's on my ballot?

Who's on my ballot?

All work

Launching 18 November 2026 · Design, data pipeline and build

A free, non-partisan voter information site for Nigeria's 2027 elections. A voter says where they will vote and sees every race on the ballot at that polling unit, with every candidate side by side, from INEC's published candidate lists.

What it does

  • More than 26,000 static pages (26,142 in the current build), generated from INEC's published candidate lists by a Python pipeline
  • Three ways to find a ballot: the code printed on a voter's card, a search by polling-unit name or landmark, or browsing state, LGA, ward and polling unit, which works with JavaScript off
  • The lookup runs in the browser across 177,299 polling units from INEC's polling-unit locator: one file per state, fetched after the state is picked, and nothing typed is sent or stored
  • HTML without the framework: Next.js renders every page at build time, then the build removes its runtime and adds one module of about 2 KB gzipped, and fails if any framework script is left
  • In Lighthouse's simulated slow-3G, low-end-phone profile, the main content appears in 2.0-2.4 s on all 11 key pages
  • The same fields and the same layout for every candidate, checked by a test whatever the party, and no rankings, polls, predictions or endorsements
  • WCAG 2.2 AA with 44px tap targets throughout, checked with axe and an automated screen-reader walk, with and without JavaScript
  • No cookies, no trackers or analytics, and no accounts

What was hard

INEC publishes its candidate lists as PDFs with a large watermark across every page and names that wrap across lines, which breaks plain text extraction. The pipeline reads each table by the position of each word, joins wrapped lines without changing INEC's text, and checks itself twice on all 104 PDFs: its row count must equal an independent count of serial numbers, and the serial numbers must run 1 to N.

INEC's own counts disagree. Its polling-unit locator lists 177,299 polling units; its published total is 176,846. The site reports the difference instead of forcing the numbers to match, and the build fails if a fresh crawl does not reproduce the reviewed baseline, which catches a truncated crawl or a change at INEC.

Some wards cannot be placed in a single State Assembly constituency, because INEC's sources contradict each other or say nothing. Rather than guess, those wards show every race they could belong to: 912 wards, 13.9% of polling units.

A page per polling unit would be about 177,000 near-identical pages. Ballots are keyed by the combination of races instead, so every lookup lands on one of 364 ballot pages for 16 January and 955 for 6 February.

Architecture

Data model
No database. A Python pipeline turns INEC's candidate PDFs, its polling-unit locator and its constituency workbook into JSON committed to the repo, so every data change is a reviewable diff. Every record carries its source, a SHA-256 hash, the page and row, and when it was retrieved.
API
None. Every page is static HTML. The site's one script fetches a single per-state file of polling units after the visitor picks a state, and matches what they type in memory.
Auth
None: no accounts, no cookies and no analytics.
Hosting
A static export on Vercel with strict security headers, including a content security policy. CI runs the pipeline's tests on INEC's PDFs, unit, browser and accessibility tests, a page-size budget and Lighthouse. A daily job checks INEC's sources and opens an issue when anything changes.