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:

  • choice must 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:

  • ranking must 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:

  1. System builds pairwise comparison matrix (who beats whom)
  2. Checks for Condorcet winner (beats all others pairwise)
  3. If no Condorcet winner, uses Ranked Pairs algorithm
  4. 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_option must be provided
  • Must be a valid option ID
  • Token is required

Validation Rules (Multiple Choice):

  • selected_options must 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_at and close_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_at timestamp set)
  • Token cannot be used again
  • Vote is recorded in ballots table

Token Requirements

Token Format

Tokens are:

  • UUIDs (Universally Unique Identifiers)
  • Stored as hashed values in database
  • Plaintext provided to voters via email

Token Lifecycle

  1. Generation: Created when admin generates tokens for an election
  2. Distribution: Sent to voters via email invite
  3. Validation: Checked when voter attempts to vote
  4. Usage: Marked as used after successful vote
  5. 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 votes
  • validateRankedVoteInput(): Validates ranked choice votes
  • validatePollVoteInput(): 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