Overview
The voting system supports three distinct ballot types, each designed for different use cases. All ballot types require a valid token to cast a vote, ensuring secure, one-person-one-vote elections.
Ballot Types
1. Simple Triple (YES/NO/ABSTAIN)
Use Case: Quick decisions, binary choices, or when you need an abstain option.
Description: Voters choose from three predefined options: YES, NO, or ABSTAIN. Results show counts and percentages for each choice.
Ballot JSON Structure:
{
"choice": "YES" | "NO" | "ABSTAIN"
}Validation Rules:
choicemust be exactly one of: “YES”, “NO”, or “ABSTAIN”- Token is required
Results Format:
{
"yes": 45,
"no": 30,
"abstain": 5,
"total": 80,
"yesPercent": 56.25,
"noPercent": 37.5,
"abstainPercent": 6.25
}When to Use:
- Board resolutions
- Policy approvals
- Simple yes/no decisions
- When abstention tracking is important
2. Ranked Choice (Condorcet)
Use Case: Elections with multiple candidates where preference ranking matters.
Description: Voters rank all candidates in order of preference (1st choice, 2nd choice, etc.). The system uses the Condorcet method with Ranked Pairs tiebreaker to determine the winner.
Ballot JSON Structure:
{
"ranking": ["candidate_id_1", "candidate_id_2", "candidate_id_3"]
}Validation Rules:
rankingmust be an array- Must include all candidates (no partial rankings)
- No duplicate candidates
- All candidate IDs must be valid
- Token is required
Example:
{
"ranking": ["cand_abc123", "cand_def456", "cand_ghi789"]
}This means: candidate cand_abc123 is 1st choice, cand_def456 is 2nd choice, cand_ghi789 is 3rd choice.
Results Format:
{
"winner": "candidate_id_1",
"ranking": ["candidate_id_1", "candidate_id_2", "candidate_id_3"],
"pairwiseMatrix": [
{
"candidateA": "candidate_id_1",
"candidateB": "candidate_id_2",
"winsA": 45,
"winsB": 35,
"margin": 10
}
],
"method": "condorcet" | "ranked_pairs"
}How It Works:
- System builds pairwise comparison matrix (who beats whom)
- Checks for Condorcet winner (beats all others pairwise)
- If no Condorcet winner, uses Ranked Pairs algorithm
- See results-calculation for details
When to Use:
- Board elections
- Officer elections
- Any election with 2+ candidates
- When preference ranking is important
3. Poll (Single or Multiple Choice)
Use Case: Custom polls with user-defined options.
Description: Create polls with custom options. Choose between single choice (radio buttons) or multiple choice (checkboxes).
Ballot JSON Structure (Single Choice):
{
"selected_option": "option_id_123"
}Ballot JSON Structure (Multiple Choice):
{
"selected_options": ["option_id_1", "option_id_2"]
}Validation Rules (Single Choice):
selected_optionmust be provided- Must be a valid option ID
- Token is required
Validation Rules (Multiple Choice):
selected_optionsmust be an array- At least one option must be selected
- No duplicate options
- All option IDs must be valid
- Token is required
Results Format:
{
"options": [
{
"option_id": "option_123",
"option_name": "Option A",
"count": 30,
"percent": 60.0
},
{
"option_id": "option_456",
"option_name": "Option B",
"count": 20,
"percent": 40.0
}
],
"total_votes": 50,
"total_selections": 50,
"response_type": "single" | "multiple"
}Note: For multiple choice polls, total_selections may exceed total_votes since voters can select multiple options. Percentages are calculated based on total votes, not selections.
When to Use:
- Survey questions
- Preference polls
- Feedback collection
- Custom voting scenarios
Voting Flow
1. Token Validation
All votes require a valid token:
- Token must exist in database
- Token must not be used (one vote per token)
- Token must belong to the election
- Election must be open (between
open_atandclose_at)
2. Ballot Submission
Endpoint: POST /e/:id/vote or POST /e/:id/vote/poll
Request Format:
{
"token": "voter_token_here",
"choice": "YES" | "NO" | "ABSTAIN", // Simple Triple
"ranking": ["cand1", "cand2"], // Ranked Choice
"selected_option": "opt1", // Poll (single)
"selected_options": ["opt1", "opt2"] // Poll (multiple)
}Response (Success):
{
"success": true,
"message": "Vote recorded successfully"
}Response (Error):
{
"success": false,
"errors": ["error message 1", "error message 2"]
}3. Token Marking
After successful vote:
- Token is marked as used (
used_attimestamp set) - Token cannot be used again
- Vote is recorded in
ballotstable
Token Requirements
Token Format
Tokens are:
- UUIDs (Universally Unique Identifiers)
- Stored as hashed values in database
- Plaintext provided to voters via email
Token Lifecycle
- Generation: Created when admin generates tokens for an election
- Distribution: Sent to voters via email invite
- Validation: Checked when voter attempts to vote
- Usage: Marked as used after successful vote
- Expiration: Tokens don’t expire, but elections do
Token Security
- Tokens are hashed using SHA-256 before storage
- Plaintext tokens are only shown in:
- Email invites (individual mode)
- “My Elections” page (batch mode)
- Admin token management (for resending)
- Tokens are validated atomically via Durable Objects (prevents double-voting)
Ballot Storage
Database Schema
CREATE TABLE ballots (
id TEXT PRIMARY KEY,
election_id TEXT NOT NULL,
ballot_type TEXT NOT NULL,
payload_json TEXT NOT NULL, -- JSON string of ballot payload
created_at INTEGER NOT NULL,
FOREIGN KEY (election_id) REFERENCES elections(id)
);Payload Storage
Ballot payloads are stored as JSON strings in payload_json column:
- Simple Triple:
{"choice":"YES"} - Ranked Choice:
{"ranking":["cand1","cand2"]} - Poll:
{"selected_option":"opt1"}or{"selected_options":["opt1","opt2"]}
Validation
Client-Side Validation
The voting pages include JavaScript validation:
- Required fields check
- Format validation
- Option/candidate existence check
Server-Side Validation
All votes are validated server-side:
- Token validation (exists, unused, correct election)
- Election status (must be open)
- Ballot format validation
- Option/candidate ID validation
- Duplicate prevention
Validation Functions
Located in src/models/ballot.ts:
validateSimpleVoteInput(): Validates simple triple votesvalidateRankedVoteInput(): Validates ranked choice votesvalidatePollVoteInput(): Validates poll votes
Error Handling
Common Errors
Invalid Token:
{
"success": false,
"errors": ["Invalid or already used token"]
}Election Closed:
{
"success": false,
"errors": ["Election is not currently open for voting"]
}Invalid Ballot:
{
"success": false,
"errors": ["choice must be YES, NO, or ABSTAIN"]
}Missing Candidates:
{
"success": false,
"errors": ["ranking must include all 3 candidates"]
}Rate Limiting
Vote submissions are rate-limited:
- 5 requests per minute per IP address
- Prevents abuse and spam
- See rate-limiting for details
See Also
- results-calculation - How results are computed
- token-management - Token validation and security
- magic-link-invites - Batch invite system
- api-reference - API endpoint documentation