Sybil-resistant voting for collective organizations — one human, one vote, enforced by World ID.
Jàmm is a self-hostable voting and governance platform for member-based organizations — diaspora associations, cooperatives, unions, political coalitions — where the hard trust problem isn't the ballot, it's who is allowed to cast one. Today these groups run elections over WhatsApp polls or on paper, with no real defense against duplicate accounts or fake members stuffing the vote.
Jàmm closes that eligibility gap with World ID proof-of-personhood, integrated as the gate on the vote itself. When a member casts a ballot on a World ID–gated election, IDKit produces a uniqueness proof on their device; Jàmm validates it server-side only — the client's verdict is never trusted — by forwarding it to the World Developer Portal's /v4/verify endpoint. On success, the returned nullifier is normalized to a canonical form and stored under a UNIQUE(election, nullifier) constraint. The guarantee: one verified human → one vote per election. World proves the proof is cryptographically valid; Jàmm proves the same human hasn't already voted in that election — so N sock-puppet accounts controlled by one person still collapse to a single vote.
Because World ID 4.0 has no official Rust SDK, the RP request-signing was reimplemented in Rust from World's spec (secp256k1, Keccak-256, EIP-191) and verified byte-for-byte against the official test vectors; the Developer Portal accepts it live.
Voting runs in dual mode, chosen per ballot: a secret commitment scheme — H(choice ‖ nonce), where the voter id is never in the hash — for anonymous ballots, or a traceable mode for transparent governance decisions. Every action (registration, membership change, World ID verification, vote) is sealed into an append-only integrity chain, H(x) = BLAKE3(SHA-256(x)). Results are independently auditable after the fact: a standalone Python verifier recomputes the entire chain — the world_id_verified events included — and confirms it without trusting Jàmm or World.
The organization's admin runs a Tauri desktop app (Rust + React, SQLCipher-encrypted local database); members self-serve from a phone or laptop through a companion web portal served by an opt-in local HTTP server. It's self-hosted by design — the association owns its members, dues, votes, and results end to end; the only outbound network call is the World ID proof check. The app also ships membership management, encrypted backups, USB hand-off between officers, server-side PDF generation (receipts, attendance sheets, ballots, member cards), and 2FA.
Built for a real context: the UI is in French, anchored in the Senegalese diaspora use case, with FCFA-native (XOF) membership-dues handling.
Jàmm is a Tauri 2 desktop app — Rust backend with a React 19 + TypeScript + Vite + Tailwind front end — backed by a local, SQLCipher-encrypted SQLite database accessed through rusqlite. The member-facing side is a second React SPA served by an opt-in Axum HTTP server that links against the same Rust core crate (jamm_lib) and runs against the same organization database, so the desktop admin and the web members operate on one organization, one database, and one integrity chain. Authentication is JWT signed with Ed25519 keys held only in memory, passwords are hashed with Argon2id, and the HTTP layer adds CSRF double-submit protection, persistent rate-limiting, and hardened security headers.
The partner technology is World ID 4.0 / IDKit, used as the eligibility gate on the vote. The 4.0 flow requires an RP-signed request (the rp_context), and since there is no official Rust SDK, we reimplemented RP request-signing from World's specification in a standalone, zero-dependency Rust crate: recoverable secp256k1 signatures via k256, Keccak-256 via tiny-keccak (not SHA3), EIP-191 prefixing, and v = recovery_id + 27. We refused to trust the implementation until it reproduced World's official test vectors byte for byte. At runtime the server signs the rp_context — the signing key stays in the environment only, wrapped in Zeroizing, never sent to the client and never logged — the browser runs @worldcoin/idkit, and the resulting uniqueness proof is forwarded verbatim and strictly server-side to POST /v4/verify/{rp_id}. The returned nullifier is normalized to a canonical 32-byte value at a single choke point, defensively extracted across the three possible response shapes (v3 nullifier_hash, v4 nullifier, v4 session_nullifier), and written under a UNIQUE(election, nullifier) constraint. A per-ballot requires_worldid flag gates the cast, and every verification is appended to a BLAKE3(SHA-256) integrity chain as an anonymous world_id_verified event holding only the election, the nullifier, and a timestamp — no identity. World ID gave us the one primitive we couldn't build ourselves, proof that a unique human is behind a vote, while we handle zero biometric or identity data.
The most hard-won parts were two opaque failures. The first live verification kept dying with a generic_error: the root cause was that reqwest sends no User-Agent by default and World's Portal sits behind a Cloudflare WAF that returns 403 with an empty body to requests without one, so our server never even reached the verify application. We isolated it with curl (no User-Agent gave 403, any User-Agent gave 400), and a single header unblocked the first live 200. On the front end, IDKit's wasm-bindgen module loads idkit_wasm_bg.wasm via new URL(..., import.meta.url); pre-bundled by Vite that path 404'd to index.html, so WASM init got the wrong MIME and HTML bytes and threw generic_error on mount — fixed with a small Vite dev middleware that serves the real wasm bytes with the application/wasm type. For the unified demo we also pointed the Axum server at the desktop app's live SQLCipher database so both faces share one chain, and wrote a small CLI to migrate that chain to canonical v2 — recomputing each block's hash and its HMAC — when the in-app rehash couldn't run with two processes on one database file. Canonical v2 drops author_id from the hashed preimage, which is exactly what lets the public chain export be recomputed by a standalone Python verifier (BLAKE3 of SHA-256) without any secret.
On AI use, to be transparent: the implementation was written with Claude Code (Anthropic) under a test-driven, spec-driven workflow, in granular per-slice commits. I directed the work and own the design — the architecture, the security model (server-side-only validation, an environment-only signing key, a per-election nullifier, chain anchoring), and the constraints. I verified the security-critical parts myself, most importantly checking the Rust RP signature against World's official test vectors and not letting the project claim it worked until the Developer Portal returned a real 200. The specification, the exact prompts I used, and a per-slice gate log are committed in the public repository (AI_WORKFLOW.md, SLICE_PLAN.md, PROMPTS.md, GATE_LOG.md), and the commit history is the authorship trail.

