All projects
03SAVoteCampus election system2025–2026Live

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
Voting booth: generating the zero-knowledge proof in the browser after choosing a candidate

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.

Campus SSO sign-inServer keeps SHA-256 of the ID
EligibilityChecked against the roll
Browser makes a secretStays on the device
Register commitmentPoseidon(ID hash, secret)
Generate proofGroth16 in a Web Worker; ballot RSA-OAEP encrypted
Server verifiessnarkjs proof check, not voted yet
One transactionKey marked used + ballot stored
TallyDecrypt with the private key after close

← 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

  1. 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 hash
    Secret
    Poseidon(…) = ?
    Web Worker builds a Groth16 proof
    commitmentproof

    What the server sees

    • commitment
    • Proof…
    • Secret
  2. 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 transaction

    UserVoteKey

    ID hashcommitmentVoted
    9f2c…1873…false
    No linking column

    Vote

    Encrypted ballotProof
    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

Admin: election management and candidate lock
Admin: election management and candidate lock
Public results page (test election)
Public results page (test election)
Voting on a phone
Voting on a phone