Anonymous voting with verified eligibility.
The e-voting system of the NCUE Student Association. Students sign in with campus SSO to confirm eligibility, and the browser generates a secret locally and registers a commitment. At voting time the browser produces a zero-knowledge proof, and the server verifies it and stores the ballot in a table that holds neither the student ID nor the commitment.
My role A team project (GDG on Campus NCUE and the Student Association's IT department). I created the project and wrote the initial specification, and built campus SSO sign-in, commitment registration, the deployment script and circuit build, Keycloak admin sign-in and permissions, and the bulletin, user guide and verification centre (later replaced by a public results page by a teammate).
- zero-knowledge proof system (BN128)
- Groth16
- zero-knowledge proof system (BN128)
- hash function inside the circuit
- Poseidon
- hash function inside the circuit
- per-election key, decrypted only at tally
- RSA-2048
- per-election key, decrypted only at tally
- separate sign-in doors: voters and admins
- 2
- separate sign-in doors: voters and admins

01
Problems
Separating eligibility from the ballot
The system must verify eligibility without keeping who voted next to what they voted.
Verifiable results
Privacy and verifiability must hold together, rather than relying on trust in the server.
One person, one vote
Each voter votes once per election, and duplicates must be rejected.
Voters and admins kept apart
Students use campus SSO and admins use the association's Keycloak; the admin interface has no voting functions.
02
Architecture
Identity is centralised (campus SSO); ballot anonymity is handled by a zero-knowledge proof and encryption. The diagram follows what the current code actually does.
← Swipe to see the whole flow →
Tech stack
- Front end
- ReactViteTailwind CSSZustand
- Back end
- NestJSPrismaPostgreSQL
- Zero knowledge
- CircomsnarkjsGroth16Poseidon
- Identity & deployment
- OIDCKeycloakDockerNginx
03
Key features
Campus SSO
Students sign in with their university account; the server keeps only a hash.
Anonymous identity
The browser creates the secret and hands over only the commitment.
Voting booth
Pick a candidate or cast a blank vote; the proof is built in a background thread.
Bulletin & results
Election bulletin, user guide and a public results page.
Admin permissions
Keycloak sign-in; super admins add, remove or promote other admins.
Elections & rolls
Create elections and candidates (lockable), import voter rolls, monitor and run lotteries.
04
Technical details
04.1
Commitments: proving knowledge without revealing it
At registration the server receives only Poseidon(ID hash, secret). At voting time the browser produces a Groth16 proof that it knows the two values behind that commitment; the server verifies the proof without ever seeing the secret.
Browser (stays with you)
ID hashSecretPoseidon(…) = ?Web Worker builds a Groth16 proofcommitmentproofWhat the server sees
- commitment
- Proof…
- Secret
04.2
Identity and ballots in separate tables
Once verified, marking the vote key as used and storing the ballot happen in one transaction. The ballot table holds only the encrypted content and the proof, with no student ID and no commitment.
- Ballots are encrypted in the browser with the election's RSA-2048 public key (OAEP, SHA-256) and decrypted only at tally.
- A failed submission gets the same response as a successful one, so responses reveal nothing about verification.
One database transactionUserVoteKey
ID hash commitment Voted 9f2c… 1873… false No linking columnVote
Encrypted ballot Proof Qm8x… {…} Zk3p… {…} Columns and values are illustrative.
Specification vs current implementation
The project documentation designs Merkle-tree membership and a nullifier into the circuit. The circuit in production proves only knowledge of the commitment's two inputs; eligibility and 'has not voted' are checked by the server. The architecture is a hybrid of centralised identity and zero-knowledge proofs, not a decentralised system.
05
Screens


