tpt-voter-portal-nz
GoRealMe-verified local body polling portal for New Zealand. Zero-knowledge-style ballot anonymity, public auditability. Go + Next.js + PostgreSQL. Electoral Act 1993 scope.
Languages
Online Voter Registration & Polling — App 3
RealMe-verified local body polling with zero-knowledge-style ballot anonymity and public auditability. Electoral Act 1993 scope: local body polling only — not for Parliamentary elections. Coordinate with the Electoral Commission before production use.
Architecture
┌─────────────────────────────────────────────────────┐
│ Next.js Frontend │
│ (TypeScript, React, Tailwind CSS) │
└──────────────────┬──────────────────────────────────┘
│ HTTP (JSON API)
┌──────────────────▼──────────────────────────────────┐
│ Go Backend (Chi) │
│ ┌────────────┐ ┌────────────┐ ┌───────────────┐ │
│ │ Auth │ │ Services │ │ Repository │ │
│ │ Handlers │ │ Layer │ │ (pgx) │ │
│ └─────┬──────┘ └─────┬──────┘ └──────┬────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ RealMe SAML Registration / PostgreSQL │
│ (Verified) Poll / Tally (pgxpool) │
└─────────────────────────────────────────────────────┘
Security Model
| Concern | Solution | |---------|----------| | One vote per person | UNIQUE(poll_id, voter_token) constraint | | No PII in ballot store | voter_token = sha256(flt_hash + poll_id + poll_salt) | | FLT never stored | Only sha256(FLT) is kept in the voters table | | Receipt verification | Random receipt_token returned to voter after casting | | Tamper-evidence | audit_root = sha256 of sorted ballot commitments | | Cross-poll unlinkability | Per-poll random salt isolates voter tokens across polls |
API Endpoints
Public (no auth)
| Method | Path | Description | |--------|------|-------------| | GET | /polls | List open polls | | GET | /polls/{id} | Poll details | | GET | /polls/{id}/results | Vote tally + audit root | | GET | /polls/{id}/audit | Full ballot list for independent verification | | GET | /polls/{id}/verify?receipt=TOKEN | Verify a receipt token | | GET | /health | Health check |
Authentication
| Method | Path | Description | |--------|------|-------------| | GET | /auth/login | Initiate RealMe login | | GET | /auth/callback | SAML callback | | GET | /auth/logout | Clear session | | GET | /auth/metadata | SAML SP metadata XML | | GET | /auth/status | Auth status (requires login) |
Protected (requires RealMe Verified identity)
| Method | Path | Description | |--------|------|-------------| | POST | /register | Register as a voter (idempotent) | | GET | /register/status | Check registration eligibility | | POST | /polls/{id}/vote | Cast a ballot | | GET | /polls/{id}/my-receipt | Retrieve your receipt token | | POST | /polls | Create a poll (admin; scope to role in production) |
Quick Start
1. Start Infrastructure
From the project root:
make dev
2. Apply Database Migration
DATABASE_URL="postgres://tptnz:tptnz_dev@localhost:5432/tptnz?sslmode=disable" \
atlas schema apply --dir "file://packages/app-voter-portal/migrations" \
--url "$DATABASE_URL" --auto-approve
Or manually:
psql "postgres://tptnz:tptnz_dev@localhost:5432/tptnz?sslmode=disable" \
-f packages/app-voter-portal/migrations/001_init.sql
3. Run the Backend
cd packages/app-voter-portal
DATABASE_URL="postgres://tptnz:tptnz_dev@localhost:5432/tptnz?sslmode=disable" \
go run ./cmd/server
API available at http://localhost:8080.
4. Run the Frontend
cd packages/app-voter-portal/web
pnpm install
pnpm dev
Frontend at http://localhost:3006.
5. Docker Compose
docker compose -f docker-compose.yml \
-f packages/app-voter-portal/docker-compose.yml up
Testing
cd packages/app-voter-portal
# Unit tests (no database required)
go test ./...
# With race detection
go test -race ./...
# Specific test
go test -v ./internal/services/ -run TestComputeAuditRoot
Environment Variables
| Variable | Default | Description | |----------|---------|-------------| | LISTEN_ADDR | :8080 | Server listen address | | DATABASE_URL | postgres://tptnz:tptnz_dev@... | PostgreSQL connection string | | REALME_ENVIRONMENT | mts | mts, ite, or production | | REALME_CERT_FILE | certs/sp.crt | SP certificate | | REALME_KEY_FILE | certs/sp.key | SP private key | | REALME_ENTITY_ID | http://localhost:8080/auth/metadata | SAML entity ID | | REALME_ACS_URL | http://localhost:8080/auth/callback | SAML ACS URL | | REALME_IDP_METADATA_FILE | (empty) | Local IdP metadata file | | REALME_IDP_METADATA_URL | http://localhost:8081/metadata | IdP metadata URL |
Audit Verification (Independent)
To independently verify a poll tally without trusting this server:
- Fetch
GET /polls/{id}/audit— get the full ballot list. - Extract all
commitmentvalues. - Sort them lexicographically.
- Concatenate and compute
sha256of the result. - Compare with
auditRootfromGET /polls/{id}/results.
A voter proves their vote was counted by finding their receiptToken in the list — without revealing their choice to anyone else (the choice index is visible only with the receipt).
RealMe Registration
To use this app with real RealMe identities (ITE or Production environments), you must register a Service Provider with the Department of Internal Affairs.
MTS (Messaging Test Site) — Development
-
Generate a self-signed certificate and key:
mkdir -p certs openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout certs/sp.key -out certs/sp.crt \ -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost" -
In MTS, no formal registration is needed — use the mock IdP in
packages/realme-go/testenv/for local development. -
Start the mock IdP:
cd packages/realme-go go run ./testenv/ -addr :8081 -
Configure the app to use the mock IdP:
REALME_IDP_METADATA_URL=http://localhost:8081/metadata
ITE (Integration Test Environment) — Pre-Production
- Log in to the RealMe Developer Portal and register a new service.
- Submit your SP metadata XML (available at
GET /auth/metadata) to DIA. - DIA will provide the ITE IdP metadata URL.
- Generate a proper certificate (not self-signed) using the naming convention:
ite.{service-name}.{org-domain}.nz - Configure environment variables:
REALME_ENVIRONMENT=ite REALME_CERT_FILE=certs/ite.sp.crt REALME_KEY_FILE=certs/ite.sp.key REALME_IDP_METADATA_URL=<DIA-provided-ITE-url>
Production
Follow the ITE steps above, substituting:
REALME_ENVIRONMENT=production
REALME_CERT_FILE=certs/prod.sp.crt
REALME_KEY_FILE=certs/prod.sp.key
REALME_IDP_METADATA_URL=<DIA-provided-prod-url>
Electoral Commission coordination required before production deployment. This system may only be used for local body polls and must be approved by the relevant local authority and the Electoral Commission.
Regulatory Notes
- Electoral Act 1993: This system is scoped to local body polls only. Parliamentary elections are governed by the Electoral Commission under separate legislation.
- Privacy Act 2020: No name, DOB, or address is stored. Only a hash of the RealMe FLT, which is itself a pseudonymous per-service identifier.
- RealMe Verified Identity: Required for registration and voting. The Assertion Service assurance level (LevelVerified) is enforced by the
RequireVerified()middleware.