openapi: 3.0.3
info:
  title: Ekklesia Voting Backend API
  description: "API for the Ekklesia voting system backend.\n\n## Versioning\n\n- **`/api/v0`** — legacy, read-only archival surface. Writes to\n  ballot/proposal/vote/voter/transaction/dashboard resources return\n  HTTP 410 Gone. Session, comments, and FAQs remain live here.\n- **`/api/v1`** — Hydra-backed live surface (broker endpoints, admin\n  lifecycle, unified ballot listing, public integrator endpoints).\n  See `openapi.v1.yaml` for the full v1 spec.\n\n## Unified listing\n\n`GET /api/v1/ballots` aggregates across sources (legacy, hydra, future)\nvia pluggable adapters. Each row includes a `source` discriminator so\nclients can render conditionally.\n\n## Authentication\n\n- Voter sessions: JWT via HTTP-only cookie or `Authorization: Bearer`.\n- Admin endpoints on `/api/v1/admin/*`: same JWT, plus an admin role claim.\n- Public endpoints on `/api/v1/public/*`: API key via\n  `Authorization: Bearer <key>` or `x-api-key`. Keys are scoped\n  (e.g. `read:ballots`, `read:results`) and rate-limited per key.\n\n## Session (unified)\nA single set of session endpoints handles both standard and multisig authentication. For multisig, include `scriptAddress` in the request body on POST (nonce) and PUT (verify). Nonce and verify requests are rate-limited.\n"
  version: 0.5.33
  contact:
    name: Adam Dean, Mad Orkestra
  license:
    name: ISC
servers:
- url: '{baseUrl}/api/v0'
  description: Ekklesia Voting API v0 instance (archival)
  variables:
    baseUrl:
      default: https://api.example.com
      description: Base URL of the Ekklesia Voting API instance
tags:
- name: Status
  description: Health and status endpoints (mounted at /health at server root)
- name: Ballots
  description: Ballot management and retrieval
- name: Proposals
  description: Proposal management and retrieval
- name: Votes
  description: Vote submission and management
- name: Comments
  description: Comment management
- name: Session
  description: Authentication and session management
- name: Dashboard
  description: User dashboard and voting checkout
- name: Transactions
  description: Transaction management
- name: Voters
  description: Voter information and statistics
- name: FAQs
  description: Frequently asked questions
paths:
  /health:
    servers:
    - url: '{baseUrl}'
      description: Health endpoints are mounted at the server root, not under /api/v0
      variables:
        baseUrl:
          default: https://api.example.com
          description: Base URL of the Ekklesia Voting API instance
    get:
      tags:
      - Status
      summary: Get system status
      description: Returns comprehensive system status (uptime, versions, database, network). Mounted at root, not under /api/v0.
      operationId: getStatus
      responses:
        '200':
          description: System status information
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusResponse'
        '500':
          $ref: '#/components/responses/ServerError'
  /health/health:
    servers:
    - url: '{baseUrl}'
      description: Health endpoints are mounted at the server root, not under /api/v0
      variables:
        baseUrl:
          default: https://api.example.com
          description: Base URL of the Ekklesia Voting API instance
    get:
      tags:
      - Status
      summary: Health check
      description: Simple health check for load balancers and monitoring. Always returns 200 with status healthy unless server is down.
      operationId: getHealth
      responses:
        '200':
          description: System is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: healthy
  /health/db:
    servers:
    - url: '{baseUrl}'
      description: Health endpoints are mounted at the server root, not under /api/v0
      variables:
        baseUrl:
          default: https://api.example.com
          description: Base URL of the Ekklesia Voting API instance
    get:
      tags:
      - Status
      summary: Database connection status
      description: Returns the current database connection status.
      operationId: getDatabaseStatus
      responses:
        '200':
          description: Database connection status
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                    - connected
                    - disconnected
                  message:
                    type: string
  /ballots/voterTypes:
    get:
      tags:
      - Ballots
      summary: Get all unique voter types
      description: Returns an array of all unique voter types from all ballots
      operationId: getVoterTypes
      responses:
        '200':
          description: Array of unique voter types
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string
        '500':
          $ref: '#/components/responses/ServerError'
  /ballots/{ballotId}/proposals:
    get:
      tags:
      - Ballots
      summary: Get proposals for a ballot
      description: Get all proposals for a specific ballot with filtering, sorting, and pagination
      operationId: getBallotProposals
      parameters:
      - name: ballotId
        in: path
        required: true
        description: The ID of the ballot
        schema:
          type: string
      - name: page
        in: query
        description: Page number for pagination
        required: false
        schema:
          type: integer
          minimum: 1
          default: 1
      - name: limit
        in: query
        description: Number of items per page (max 100)
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 10
      - name: search
        in: query
        description: Search term for proposal title or ID
        required: false
        schema:
          type: string
      - name: sort
        in: query
        description: Sort field
        required: false
        schema:
          type: string
          enum:
          - title
          - commentCount
          - voteCount
          default: _id
      - name: direction
        in: query
        description: Sort direction
        required: false
        schema:
          type: string
          enum:
          - asc
          - desc
          default: desc
      - name: hasVoted
        in: query
        description: Filter by whether authenticated user has voted (only works when authenticated)
        required: false
        schema:
          type: string
          enum:
          - "true"
          - "false"
      - name: tags
        in: query
        description: Filter by tags (comma-separated)
        required: false
        schema:
          type: string
      - name: categories
        in: query
        description: Filter by categories (comma-separated)
        required: false
        schema:
          type: string
      responses:
        '200':
          description: List of proposals with pagination and filter metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Proposal'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  sort:
                    type: object
                    properties:
                      field:
                        type: string
                      direction:
                        type: string
                  filters:
                    type: object
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
  /ballots/{ballotId}/categories:
    get:
      tags:
      - Ballots
      summary: Get unique categories for a ballot
      description: Get all unique categories from proposals in a specific ballot
      operationId: getBallotCategories
      parameters:
      - name: ballotId
        in: path
        required: true
        description: The ID of the ballot
        schema:
          type: string
      responses:
        '200':
          description: List of unique categories
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
  /ballots/{ballotId}/tags:
    get:
      tags:
      - Ballots
      summary: Get unique tags for a ballot
      description: Get all unique tags from proposals in a specific ballot
      operationId: getBallotTags
      parameters:
      - name: ballotId
        in: path
        required: true
        description: The ID of the ballot
        schema:
          type: string
      responses:
        '200':
          description: List of unique tags
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
  /ballots/{ballotId}:
    get:
      tags:
      - Ballots
      summary: Get a specific ballot
      description: Get a specific ballot by ID with voter validation if token is present
      operationId: getBallot
      parameters:
      - name: ballotId
        in: path
        required: true
        description: The ID of the ballot to retrieve
        schema:
          type: string
      responses:
        '200':
          description: The ballot object with additional voter-specific data if authenticated
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Ballot'
                - type: object
                  properties:
                    voterValidated:
                      type: boolean
                      description: Voter eligibility flag (only when authenticated).
                    votingPower:
                      type: number
                      description: Authenticated voter's individual voting power on this ballot (only when authenticated and eligible).
                    votingPowerByGroup:
                      type: object
                      description: 'Per-voter-group total voting power (canonical post-violet-clever-noether shape).

                        Honors `Ballot.votingPowerSource` (script vs snapshot vs admin-uploaded);

                        falls back to a one-shot live computation when no snapshot rows exist yet.

                        '
                      additionalProperties:
                        type: number
                    eligibleVoterCountByGroup:
                      type: object
                      description: Per-voter-group count of eligible voters.
                      additionalProperties:
                        type: integer
                    activeVotingPowerByGroup:
                      type: object
                      description: Per-voter-group voting power among voters who have actually submitted a vote.
                      additionalProperties:
                        type: number
                    activeVoterCountByGroup:
                      type: object
                      description: Per-voter-group count of voters who have actually submitted a vote.
                      additionalProperties:
                        type: integer
                    votingPowerSourceInfo:
                      type: object
                      description: 'Provenance of the voting-power numbers for this ballot — which source

                        actually populated the per-group fields (script-derived, snapshot row,

                        admin-uploaded, or live fallback).

                        '
                    totalAllowedVoterCount:
                      type: number
                      deprecated: true
                      description: 'Degenerate sum of `eligibleVoterCountByGroup` — kept populated for one

                        release cycle so legacy frontends can migrate to the *ByGroup keys.

                        '
                    totalVotingPower:
                      type: number
                      deprecated: true
                      description: 'Degenerate sum of `votingPowerByGroup` — kept populated for one

                        release cycle so legacy frontends can migrate to the *ByGroup keys.

                        '
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
  /ballots:
    get:
      tags:
      - Ballots
      summary: Get all ballots
      description: Get all ballots with pagination, filtering, and search capabilities
      operationId: getBallots
      parameters:
      - name: voterType
        in: query
        description: Filter by voter type
        required: false
        schema:
          type: string
      - name: status
        in: query
        description: Filter by status
        required: false
        schema:
          type: string
          enum:
          - live
          - closed
          - upcoming
      - name: search
        in: query
        description: Search term for ballot title or ID
        required: false
        schema:
          type: string
          minLength: 1
          maxLength: 100
      - name: page
        in: query
        description: Page number for pagination
        required: false
        schema:
          type: integer
          minimum: 1
          default: 1
      - name: limit
        in: query
        description: Number of items per page (max 100)
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 10
      responses:
        '200':
          description: List of ballots with pagination metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Ballot'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/ServerError'
  /proposals/{proposalId}/comments:
    get:
      tags:
      - Proposals
      summary: Get comments for a proposal
      description: Get all comments for a specific proposal sorted by creation date
      operationId: getProposalComments
      parameters:
      - name: proposalId
        in: path
        required: true
        description: The ID of the proposal
        schema:
          type: string
      responses:
        '200':
          description: List of comments for the proposal
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Comment'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
  /proposals/{proposalId}/results/grouped:
    get:
      tags:
      - Proposals
      summary: Get voting results by voter group
      description: Get voting results for a proposal broken down by voter group. Returns stored results when available, otherwise computes on-the-fly.
      operationId: getProposalResultsGrouped
      parameters:
      - name: proposalId
        in: path
        required: true
        description: The ID of the proposal
        schema:
          type: string
      responses:
        '200':
          description: Results per voter group
          content:
            application/json:
              schema:
                type: object
                properties:
                  proposalId:
                    type: string
                    description: The proposal ID
                  groups:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        results:
                          type: array
                          items:
                            type: object
                            properties:
                              value:
                                type: number
                                description: Vote option value/id
                              label:
                                type: string
                              count:
                                type: number
                              votingPower:
                                type: number
                        totalVotes:
                          type: number
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
  /proposals/{proposalId}/results:
    get:
      tags:
      - Proposals
      summary: Get voting results for a proposal
      description: Get voting results for a specific proposal with vote counts and voting power
      operationId: getProposalResults
      parameters:
      - name: proposalId
        in: path
        required: true
        description: The ID of the proposal
        schema:
          type: string
      responses:
        '200':
          description: The proposal object with voting results
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Proposal'
                - type: object
                  properties:
                    results:
                      type: array
                      items:
                        type: object
                        properties:
                          id:
                            oneOf:
                            - type: number
                            - type: string
                              enum:
                              - abstain
                            description: Vote option id (matches `voteOptions[].id`); the literal "abstain" appears as an explicit bucket when the proposal allows abstaining.
                          label:
                            type: string
                            description: Vote option label
                          count:
                            type: number
                            description: Number of votes for this option
                          votingPower:
                            type: number
                            description: Total voting power for this option
                    totalVotes:
                      type: number
                      description: Total number of votes cast
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
  /proposals/{proposalId}/short:
    get:
      tags:
      - Proposals
      summary: Get a shortened version of a proposal
      description: Get a shortened version of a proposal without detailed data
      operationId: getProposalShort
      parameters:
      - name: proposalId
        in: path
        required: true
        description: The ID of the proposal to retrieve
        schema:
          type: string
      responses:
        '200':
          description: The proposal object without detailed data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Proposal'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
  /proposals/{proposalId}:
    get:
      tags:
      - Proposals
      summary: Get a proposal by ID
      description: Get a proposal by ID with voting statistics and user vote if authenticated
      operationId: getProposal
      parameters:
      - name: proposalId
        in: path
        required: true
        description: The ID of the proposal to retrieve
        schema:
          type: string
      responses:
        '200':
          description: The proposal object with additional voting statistics
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Proposal'
                - type: object
                  properties:
                    voterVote:
                      type: array
                      nullable: true
                      items:
                        oneOf:
                        - type: number
                        - type: string
                          enum:
                          - abstain
                      description: Array of vote option IDs the authenticated voter selected (or null if not voted)
                    ballotStatus:
                      type: string
                    results:
                      type: object
                      nullable: true
                    totalVotes:
                      type: number
                    totalVoterCount:
                      type: number
                    totalVotingPower:
                      type: number
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
  /vote/{proposalId}:
    post:
      tags:
      - Votes
      summary: Submit or update a vote
      description: Submit or update a vote on a specific proposal
      operationId: submitVote
      security:
      - cookieAuth: []
      - bearerAuth: []
      parameters:
      - name: proposalId
        in: path
        required: true
        description: ID of the proposal to vote on
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - vote
              properties:
                vote:
                  type: array
                  items:
                    oneOf:
                    - type: number
                    - type: string
                      enum:
                      - abstain
                  description: Array of vote option IDs (numbers) or "abstain" string
                  minItems: 1
      responses:
        '200':
          description: The saved vote object with indication if vote was changed
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Vote'
                - type: object
                  properties:
                    changes:
                      type: boolean
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: 'Vote rejected because the ballot is outside its voting window.

            `code` is `VOTING_WINDOW_NOT_OPEN` before `votePeriodStart` or

            `VOTING_WINDOW_CLOSED` at/after `votePeriodEnd`.

            '
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: error
                  code:
                    type: string
                    enum:
                    - VOTING_WINDOW_NOT_OPEN
                    - VOTING_WINDOW_CLOSED
                  message:
                    type: string
        '500':
          $ref: '#/components/responses/ServerError'
  /comments/{commentId}/replies:
    get:
      tags:
      - Comments
      summary: List replies to a comment
      description: Paginated replies. Parent must exist and be live. Sorted by createdAt asc.
      operationId: listCommentReplies
      parameters:
      - name: commentId
        in: path
        required: true
        schema:
          type: string
      - name: status
        in: query
        schema:
          type: string
      - name: page
        in: query
        schema:
          type: integer
          minimum: 1
          default: 1
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 10
      responses:
        '200':
          description: Paginated replies with meta
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CommentResponse'
                  meta:
                    type: object
                    properties:
                      page:
                        type: integer
                      limit:
                        type: integer
                      total:
                        type: integer
                      totalPages:
                        type: integer
                      hasNextPage:
                        type: boolean
                      hasPreviousPage:
                        type: boolean
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
  /comments/{commentId}/like:
    post:
      tags:
      - Comments
      summary: Toggle like on a comment
      description: Toggle like. 201 when like added, 200 when removed. Live comment only; before vote feedbackEndDate.
      operationId: toggleCommentLike
      security:
      - cookieAuth: []
      - bearerAuth: []
      parameters:
      - name: commentId
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Like removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  message:
                    type: string
                    example: Like removed.
                  liked:
                    type: boolean
                    example: false
                  likeCount:
                    type: integer
        '201':
          description: Like added
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  message:
                    type: string
                    example: Comment liked.
                  liked:
                    type: boolean
                    example: true
                  likeCount:
                    type: integer
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
  /comments/{commentId}/withdraw:
    put:
      tags:
      - Comments
      summary: Withdraw a comment (admin)
      description: Vote admin withdraws a live comment. Body category required. Until feedbackEndDate.
      operationId: withdrawComment
      security:
      - cookieAuth: []
      - bearerAuth: []
      parameters:
      - name: commentId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - category
              properties:
                category:
                  type: string
                  enum:
                  - Inappropriate content
                  - Spam
                  - Policy violation
                  - Duplicate
                  - Other
                comment:
                  type: string
                  description: Optional reason note
      responses:
        '200':
          description: Withdrawn comment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Comment'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
  /comments/{commentId}:
    get:
      tags:
      - Comments
      summary: Get a single comment
      description: Get comment by ID. Public sees live only; author sees own in any status; admin can filter by status.
      operationId: getComment
      parameters:
      - name: commentId
        in: path
        required: true
        schema:
          type: string
      - name: status
        in: query
        description: Status filter (admins only)
        schema:
          type: string
      responses:
        '200':
          description: Comment with author, replyCount, likeCount, userLiked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommentResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
    put:
      tags:
      - Comments
      summary: Update comment content
      description: Author only; within 15 minutes of creation.
      operationId: updateComment
      security:
      - cookieAuth: []
      - bearerAuth: []
      parameters:
      - name: commentId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - content
              properties:
                content:
                  type: string
                  maxLength: 2000
      responses:
        '200':
          description: Updated comment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Comment'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
  /comments:
    get:
      tags:
      - Comments
      summary: List top-level comments for a proposal
      description: 'Paginated top-level comments for a proposal. Query parameter `proposal` (proposalId) is required.

        Public users see only live comments. Admins can filter by status (live, withdrawn, withdrawnByAdmin).

        Response includes author, replyCount, likeCount, userLiked (when authenticated).

        '
      operationId: listComments
      parameters:
      - name: proposal
        in: query
        required: true
        description: Proposal ID
        schema:
          type: string
      - name: status
        in: query
        description: Filter by status (admins only; live, withdrawn, withdrawnByAdmin)
        schema:
          type: string
      - name: sort
        in: query
        description: Sort field
        schema:
          type: string
          enum:
          - date
          - replyCount
          - likeCount
          default: date
      - name: direction
        in: query
        description: Sort direction
        schema:
          type: string
          enum:
          - asc
          - desc
          default: desc
      - name: userType
        in: query
        description: Filter by author type (comma-separated; proposer, admin, drep)
        schema:
          type: string
      - name: page
        in: query
        schema:
          type: integer
          minimum: 1
          default: 1
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 10
      responses:
        '200':
          description: Paginated comments with meta
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CommentResponse'
                  meta:
                    type: object
                    properties:
                      page:
                        type: integer
                      limit:
                        type: integer
                      total:
                        type: integer
                      totalPages:
                        type: integer
                      hasNextPage:
                        type: boolean
                      hasPreviousPage:
                        type: boolean
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
      - Comments
      summary: Create a comment
      description: Create a comment on a live proposal. Auth required. Optional parentId for replies. Before vote feedbackEndDate.
      operationId: createComment
      security:
      - cookieAuth: []
      - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - proposalId
              - content
              properties:
                proposalId:
                  type: string
                  description: Proposal ID
                content:
                  type: string
                  description: Comment content (max 2000 characters)
                  maxLength: 2000
                parentId:
                  type: string
                  description: Optional; parent comment ID for replies
      responses:
        '201':
          description: Created comment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Comment'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
  /session:
    get:
      tags:
      - Session
      summary: Validate session
      description: Validate JWT token and return voter ID plus user profile (name, lastLogin), admin flag, native-script context, and active vote-package summary when available
      operationId: getSession
      security:
      - cookieAuth: []
      - bearerAuth: []
      responses:
        '200':
          description: Voter ID, optional user profile, admin flag, native-script context, and pending vote packages
          content:
            application/json:
              schema:
                type: object
                required:
                - userId
                - isAdmin
                - nativeScript
                - pendingPackages
                properties:
                  userId:
                    type: string
                  isAdmin:
                    type: boolean
                    description: True when the session voter is on the admin allowlist.
                  name:
                    type: string
                    nullable: true
                    description: DRep name or handle (from User, when set)
                  lastLogin:
                    type: string
                    format: date-time
                    nullable: true
                    description: Last login timestamp (from User)
                  nativeScript:
                    nullable: true
                    description: 'JSON definition of the multisig native script tied to this session

                      (script address voter); null for ordinary key-based voters. Always

                      present so the frontend can rely on the field.

                      '
                    type: object
                  pendingPackages:
                    type: array
                    description: 'Up to 25 most-recently-updated non-terminal v1 vote packages owned

                      by this voter, surfaced for the wallet-reconnect / multisig-coordination

                      UX. Empty array when there are none.

                      '
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        ballotId:
                          type: string
                          nullable: true
                        status:
                          type: string
                          enum:
                          - draft
                          - awaiting-signatures
                          - awaiting-submission
                          - broker-submitted
                          - failed
                        nonce:
                          type: integer
                        signatureCount:
                          type: integer
                        isMultisig:
                          type: boolean
                        updatedAt:
                          type: string
                          format: date-time
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
      - Session
      summary: Request authentication nonce
      description: 'Request nonce for signing. Single endpoint for standard and multisig.

        - Standard: send signerAddress and signType; userId is the signer address.

        - Multisig: also send scriptAddress (CIP129 script address); userId in response is the script address.

        Rate-limited (nonce requests per minute).

        '
      operationId: requestNonce
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - signerAddress
              - signType
              properties:
                signerAddress:
                  type: string
                  description: The address of the signer
                signType:
                  type: string
                  description: Type of signature (e.g., 'drep', 'stake', 'pool')
                scriptAddress:
                  type: string
                  description: For multisig only; CIP129 script address. When provided, identity is the script address.
      responses:
        '200':
          description: Nonce to sign and voter identification data
          content:
            application/json:
              schema:
                type: object
                properties:
                  dataHex:
                    type: string
                    description: Nonce to sign (hex encoded)
                  userId:
                    type: string
                    description: Signer address (standard) or script address (multisig)
                  userIdHex:
                    type: string
                  signerAddressHex:
                    type: string
                  calidusID:
                    type: string
                    nullable: true
                    description: Calidus ID for pool signers (only present for pool signType)
                  scriptAddress:
                    type: string
                    description: Present only for multisig; CIP129 script address
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
    put:
      tags:
      - Session
      summary: Verify signature and login
      description: 'Verify signature and issue JWT. Single endpoint for standard and multisig.

        - Standard: send signerAddress, signType, signature.

        - Multisig: also send scriptAddress. Verifies signer is party to script.

        On success, User lastLogin (and name for drep/stake/addr when fetched) is updated. Rate-limited.

        '
      operationId: verifySignature
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - signerAddress
              - signType
              - signature
              properties:
                signerAddress:
                  type: string
                signType:
                  type: string
                scriptAddress:
                  type: string
                  description: For multisig only; CIP129 script address
                signature:
                  $ref: '#/components/schemas/Signature'
      responses:
        '200':
          description: JWT token and expiration information
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                  expiresIn:
                    type: string
                    format: date-time
                  userId:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
    delete:
      tags:
      - Session
      summary: Logout
      description: Logout by clearing authentication cookie
      operationId: logout
      security:
      - cookieAuth: []
      - bearerAuth: []
      responses:
        '200':
          description: Success message confirming logout
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  message:
                    type: string
                    example: Logged out successfully
        '401':
          $ref: '#/components/responses/Unauthorized'
  /dashboard/ballots:
    get:
      tags:
      - Dashboard
      summary: Get ballots for voter
      description: Get all ballots the authenticated voter can vote on or has already voted on
      operationId: getDashboardBallots
      security:
      - cookieAuth: []
      - bearerAuth: []
      responses:
        '200':
          description: List of ballots with voter-specific information (voting power)
          content:
            application/json:
              schema:
                type: array
                items:
                  allOf:
                  - $ref: '#/components/schemas/Ballot'
                  - type: object
                    properties:
                      votingPower:
                        type: number
        '401':
          $ref: '#/components/responses/Unauthorized'
  /dashboard/pending:
    get:
      tags:
      - Dashboard
      summary: Get pending votes
      description: Get all pending votes for the authenticated voter
      operationId: getPendingVotes
      security:
      - cookieAuth: []
      - bearerAuth: []
      responses:
        '200':
          description: List of pending votes or message if no pending votes exist
          content:
            application/json:
              schema:
                oneOf:
                - type: object
                  properties:
                    message:
                      type: string
                      example: no pending votes
                - type: array
                  items:
                    $ref: '#/components/schemas/Vote'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /dashboard/{ballotId}/checkout/multisig/{transactionId}:
    post:
      tags:
      - Dashboard
      summary: Get existing multisig transaction
      description: Request checkout for multisig with an existing transaction ID; returns the existing pending transaction data for that ballot and voter.
      operationId: getMultisigTransaction
      security:
      - cookieAuth: []
      - bearerAuth: []
      parameters:
      - name: ballotId
        in: path
        required: true
        description: ID of the ballot
        schema:
          type: string
      - name: transactionId
        in: path
        required: true
        description: ID of the existing pending transaction to retrieve
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - scriptAddress
              - signerAddress
              - signType
              properties:
                scriptAddress:
                  type: string
                signerAddress:
                  type: string
                signType:
                  type: string
      responses:
        '200':
          description: Existing transaction data for signing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /dashboard/{ballotId}/checkout/multisig:
    post:
      tags:
      - Dashboard
      summary: Request checkout for multisig (create or get transaction)
      description: Request checkout for a ballot using multisig. Creates new transaction or returns existing one when transactionId is provided.
      operationId: requestMultisigCheckout
      security:
      - cookieAuth: []
      - bearerAuth: []
      parameters:
      - name: ballotId
        in: path
        required: true
        description: ID of the ballot to checkout
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - scriptAddress
              - signerAddress
              - signType
              properties:
                scriptAddress:
                  type: string
                  description: Address of the multisig script
                signerAddress:
                  type: string
                signType:
                  type: string
      responses:
        '200':
          description: Transaction data for signing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
      - Dashboard
      summary: Submit multisig signature
      description: Submit a signature for a multisig transaction, finalizing votes if all required signatures are present
      operationId: submitMultisigSignature
      security:
      - cookieAuth: []
      - bearerAuth: []
      parameters:
      - name: ballotId
        in: path
        required: true
        description: ID of the ballot
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - signerAddress
              - signType
              - scriptAddress
              - data
              - signature
              properties:
                signerAddress:
                  type: string
                signType:
                  type: string
                scriptAddress:
                  type: string
                data:
                  type: string
                  description: Merkle root of the transaction data
                signature:
                  $ref: '#/components/schemas/Signature'
      responses:
        '200':
          description: Confirmation of submitted signature or finalized votes
          content:
            application/json:
              schema:
                oneOf:
                - type: object
                  properties:
                    status:
                      type: string
                      example: info
                    message:
                      type: string
                      example: MultiSig not complete yet
                - type: object
                  properties:
                    status:
                      type: string
                      example: ok
                    message:
                      type: string
                      example: Votes submitted
                    transaction:
                      type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /dashboard/{ballotId}/checkout:
    post:
      tags:
      - Dashboard
      summary: Request checkout for a ballot
      description: Request checkout for a ballot, creating transaction data for signing
      operationId: requestCheckout
      security:
      - cookieAuth: []
      - bearerAuth: []
      parameters:
      - name: ballotId
        in: path
        required: true
        description: ID of the ballot to checkout
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - signerAddress
              - signType
              properties:
                signerAddress:
                  type: string
                  description: Address of the signer
                signType:
                  type: string
                  description: Type of signature (e.g., 'drep')
      responses:
        '200':
          description: Transaction data for signing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
      - Dashboard
      summary: Submit signed transaction
      description: Submit a signed transaction to finalize votes for a ballot
      operationId: submitTransaction
      security:
      - cookieAuth: []
      - bearerAuth: []
      parameters:
      - name: ballotId
        in: path
        required: true
        description: ID of the ballot
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - signerAddress
              - signType
              - data
              - signature
              properties:
                signerAddress:
                  type: string
                signType:
                  type: string
                data:
                  type: string
                  description: Merkle root of the transaction data
                signature:
                  $ref: '#/components/schemas/Signature'
      responses:
        '200':
          description: Confirmation of submitted votes with transaction ID
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  message:
                    type: string
                    example: Votes submitted
                  transaction:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /dashboard:
    get:
      tags:
      - Dashboard
      summary: Get dashboard information
      description: Get authenticated voter stats including last login time and pending votes count
      operationId: getDashboard
      security:
      - cookieAuth: []
      - bearerAuth: []
      responses:
        '200':
          description: Voter information including voter ID, last login timestamp, and pending votes count
          content:
            application/json:
              schema:
                type: object
                properties:
                  userId:
                    type: string
                  lastLogin:
                    type: string
                    format: date-time
                    nullable: true
                  multiSig:
                    type: boolean
                    description: Whether the voter is using multisig authentication
                  pendingVotesCount:
                    type: number
                    description: Number of pending (unsubmitted) votes for the voter
        '401':
          $ref: '#/components/responses/Unauthorized'
  /transactions/{transactionId}:
    get:
      tags:
      - Transactions
      summary: Get a specific transaction
      description: Get a specific transaction by ID for the authenticated user
      operationId: getTransaction
      security:
      - cookieAuth: []
      - bearerAuth: []
      parameters:
      - name: transactionId
        in: path
        required: true
        description: ID of the transaction to retrieve
        schema:
          type: string
      responses:
        '200':
          description: The requested transaction object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Transaction'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /transactions:
    get:
      tags:
      - Transactions
      summary: Get all transactions
      description: Get all transactions for the authenticated user
      operationId: getTransactions
      security:
      - cookieAuth: []
      - bearerAuth: []
      responses:
        '200':
          description: List of transactions for the user sorted by update time (descending)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Transaction'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /voters/types:
    get:
      tags:
      - Voters
      summary: Get voter type counts
      description: Get counts of different voter types (stake, drep, pool)
      operationId: getVoterTypeCounts
      responses:
        '200':
          description: Array of objects with voter type and count
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    type:
                      type: string
                      enum:
                      - stake
                      - drep
                      - pool
                    count:
                      type: number
        '500':
          $ref: '#/components/responses/ServerError'
  /voters/{userId}:
    get:
      tags:
      - Voters
      summary: Get voter details
      description: Get detailed information about a specific voter including voting history
      operationId: getVoter
      parameters:
      - name: userId
        in: path
        required: true
        description: The ID of the voter to retrieve
        schema:
          type: string
      responses:
        '200':
          description: The voter object with voting history and statistics
          content:
            application/json:
              schema:
                type: object
                properties:
                  voterType:
                    type: string
                    enum:
                    - stake
                    - drep
                    - pool
                  userId:
                    type: string
                  votes:
                    type: array
                    items:
                      type: object
                      properties:
                        _id:
                          type: string
                        title:
                          type: string
                        votePeriodStart:
                          type: string
                          format: date-time
                        votePeriodEnd:
                          type: string
                          format: date-time
                        votingPower:
                          type: number
                        status:
                          type: string
                        proposals:
                          type: array
                          items:
                            type: object
                            properties:
                              proposalId:
                                type: string
                              title:
                                type: string
                              voteType:
                                type: string
                                enum:
                                - choice
                                - multi-choice
                                - ranked
                                - scale
                                - likert
                                - weighted
                                - budget
                                description: 'Voting method for the proposal. Drives the shape of the

                                  `vote` array below — clients should dispatch on this rather

                                  than sniffing array contents.

                                  '
                              vote:
                                type: array
                                description: 'Per-vote-type projection of the voter''s submission:

                                  - `choice` / `multi-choice` / `ranked`: array of option labels (strings).

                                  - `scale`: a single-element array carrying the numeric score (zero is preserved).

                                  - `likert` / `weighted`: array of `{ option, optionLabel, value }` objects, one per voted-on option. `value` is the rating (likert) or point allocation (weighted).

                                  - Any abstain (any voteType): `["Abstain"]`.

                                  '
                                items:
                                  oneOf:
                                  - type: string
                                  - type: number
                                  - type: object
                                    properties:
                                      option:
                                        type: string
                                        description: voteOption id
                                      optionLabel:
                                        type: string
                                        description: Denormalized label for the option, copied from the proposal's voteOptions catalogue.
                                      value:
                                        type: number
                                        description: Rating (likert) or integer point allocation (weighted).
                                    required:
                                    - option
                                    - optionLabel
                                    - value
                  ballotsVoted:
                    type: number
                    description: Number of ballots the voter has voted on
                  proposalsVoted:
                    type: number
                    description: Total number of proposals the voter has voted on across all ballots
                  lastVoteDate:
                    type: string
                    format: date-time
                    nullable: true
                    description: Date of the most recent vote
                  lastLogin:
                    type: string
                    format: date-time
                    nullable: true
                    description: Last login timestamp
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
  /voters:
    get:
      tags:
      - Voters
      summary: Get paginated list of voters
      description: Get a paginated list of voters with filtering, sorting, and search capabilities
      operationId: getVoters
      parameters:
      - name: page
        in: query
        description: Page number for pagination
        required: false
        schema:
          type: integer
          minimum: 1
          default: 1
      - name: limit
        in: query
        description: Number of items per page (max 100)
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 25
      - name: search
        in: query
        description: Search term for voter ID
        required: false
        schema:
          type: string
      - name: sort
        in: query
        description: Sort field
        required: false
        schema:
          type: string
          enum:
          - userId
          - votes
          - lastLogin
          default: votes
      - name: direction
        in: query
        description: Sort direction
        required: false
        schema:
          type: string
          enum:
          - asc
          - desc
          default: desc
      responses:
        '200':
          description: List of voters with pagination metadata
          content:
            application/json:
              schema:
                oneOf:
                - type: object
                  properties:
                    status:
                      type: string
                      example: msg
                    message:
                      type: string
                      example: No voters found
                - type: object
                  properties:
                    data:
                      type: array
                      items:
                        type: object
                        properties:
                          userId:
                            type: string
                          votes:
                            type: number
                            description: Number of votes cast by this voter
                          lastLogin:
                            type: string
                            format: date-time
                            nullable: true
                    pagination:
                      $ref: '#/components/schemas/Pagination'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/ServerError'
  /faqs:
    get:
      tags:
      - FAQs
      summary: Get all live FAQs
      description: Get all live FAQs with search and filtering capabilities. Only FAQs with is_live set to true are returned.
      operationId: getFAQs
      parameters:
      - name: search
        in: query
        description: Search term for FAQ title or content
        required: false
        schema:
          type: string
          minLength: 1
          maxLength: 100
      - name: tags
        in: query
        description: Filter by tags (comma-separated, e.g., 'voter,proposer')
        required: false
        schema:
          type: string
          example: voter,proposer
      - name: featured
        in: query
        description: Filter by featured status (true or false)
        required: false
        schema:
          type: string
          enum:
          - "true"
          - "false"
      responses:
        '200':
          description: List of live FAQs matching the search and filter criteria
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/FAQ'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/ServerError'
components:
  securitySchemes:
    cookieAuth:
      type: apiKey
      in: cookie
      name: token
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  schemas:
    StatusResponse:
      type: object
      description: 'Comprehensive system status information including server health, database connection status,

        network information, and runtime metrics. Used by monitoring tools and system administrators

        to assess the operational state of the API server.

        '
      properties:
        status:
          type: string
          description: Overall system status (e.g., "operational", "degraded", "down")
          example: operational
        message:
          type: string
          description: Human-readable status message describing the current system state
          example: System is operational
        timestamp:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the status was generated
        environment:
          type: string
          description: Current deployment environment (e.g., "development", "production")
        network:
          type: string
          description: Name of the blockchain network (e.g., "mainnet", "preprod", "preview")
        networkId:
          type: integer
          description: Numeric identifier for the blockchain network
        server:
          type: object
          description: Server runtime information and metrics
          properties:
            uptime:
              type: string
              description: Human-readable server uptime (e.g., "5d 12h 30m 15s")
            uptimeSeconds:
              type: number
              description: Server uptime in seconds since last restart
            version:
              type: string
              description: Version of the API server application
            nodeVersion:
              type: string
              description: Version of Node.js runtime
            memoryUsage:
              type: object
              description: Current memory usage statistics
              properties:
                rss:
                  type: string
                  description: Resident Set Size - total memory allocated for the process
                heapTotal:
                  type: string
                  description: Total size of allocated heap memory
                heapUsed:
                  type: string
                  description: Amount of heap memory currently in use
        frontend:
          type: string
          description: Version of the frontend application (if available)
        database:
          type: object
          description: Database connection status and health information
          properties:
            status:
              type: string
              enum:
              - connected
              - disconnected
              description: Current database connection state
            message:
              type: string
              description: Human-readable message about database connection status
    Ballot:
      type: object
      description: 'Represents a registered ballot in the voting system. A ballot defines a voting period,

        eligible voter types, and contains multiple proposals that voters can vote on. Ballots

        can be in "upcoming", "live", or "closed" status based on the current time relative

        to votePeriodStart and votePeriodEnd dates.

        '
      properties:
        _id:
          type: string
          description: Unique identifier for the ballot
        title:
          type: string
          description: Title of the ballot
        description:
          type: string
          description: Detailed description of what the ballot is about
        ipfsHash:
          type: string
          nullable: true
          description: IPFS hash of the ballot metadata (optional, for decentralized storage)
        voterType:
          type: string
          description: 'Legacy display label for eligible voters (e.g., ''stake'', ''drep'', ''pool'', ''any'').

            Retained for backwards-compatible list filtering (`?voterType=`). The

            authoritative per-group eligibility + power-source declaration lives in

            `voterGroups`; new clients should prefer that field.

            '
        voterGroups:
          type: array
          description: 'Authoritative per-group eligibility + power-source declaration. A single

            ballot can declare multiple groups simultaneously (e.g. RSS v2 enables

            both `drep` and `pool`). `hydraPrepare` translates this into Hydra''s

            `roleWeighting` object at /prepare time. Valid (group, powerSource)

            combinations:

            - drep  → CredentialBased | StakeBased

            - pool  → CredentialBased | StakeBased | PledgeBased

            - stake → StakeBased

            '
          items:
            type: object
            required:
            - group
            - powerSource
            properties:
              group:
                type: string
                enum:
                - drep
                - pool
                - stake
              powerSource:
                type: string
                enum:
                - CredentialBased
                - StakeBased
                - PledgeBased
              requirements:
                type: object
                nullable: true
                description: |
                  Optional per-group eligibility gate. Validator-interpreted;
                  today only `stake` honors this. Accepts `mustExist: boolean`,
                  `allowedPools: string[]`, and
                  `tokenHoldings: [{policyId, assetName?, minQuantity}]`.
                additionalProperties: true
        voterDescription:
          type: string
          description: Human-readable description of who is eligible to vote in this ballot
        votePeriodStart:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the voting period begins
        votePeriodEnd:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the voting period ends
        voteFilters:
          type: boolean
          description: Whether filtering options (tags, categories) are enabled for proposals in this ballot
        voteWeighted:
          type: boolean
          description: Whether votes in this ballot are weighted by voting power (true) or counted equally (false)
        proposalPeriodStart:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the proposal submission period begins
        proposalPeriodEnd:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the proposal submission period ends
        resultTxHash:
          type: string
          nullable: true
          description: Blockchain transaction hash where final results were recorded (null if not yet finalized)
        status:
          type: string
          enum:
          - live
          - closed
          - upcoming
          description: 'Current status of the ballot:

            - "upcoming": Voting period has not started yet

            - "live": Voting period is currently active

            - "closed": Voting period has ended

            '
        createdAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the ballot was created
        updatedAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the ballot was last updated
    Proposal:
      type: object
      description: 'Represents a registered proposal within a ballot. Proposals are items that voters can vote on.

        Each proposal belongs to a specific ballot and contains details about what''s being voted on,

        along with configurable voting options. Proposals support multiple vote types including

        choice (single-pick Yes/No or candidate election), multi-choice (pick min..max), budget (knapsack with option costs), weighted (point allocation), ranked, scale (numeric range), and likert (per-option rating) voting.

        '
      properties:
        _id:
          type: string
          description: Unique identifier for the proposal
        ballotId:
          type: string
          description: ID of the ballot this proposal belongs to
        ipfsHash:
          type: string
          nullable: true
          description: IPFS hash of the proposal metadata (optional, for decentralized storage)
        title:
          type: string
          description: Title of the proposal
        description:
          type: string
          description: Detailed description of the proposal content
        categories:
          type: array
          items:
            type: string
          description: Array of category labels for organizing proposals (e.g., ["Governance", "Treasury"])
        tags:
          type: array
          items:
            type: string
          description: Array of tag labels for filtering and searching proposals
        data:
          type: object
          nullable: true
          description: Additional structured data related to the proposal (format varies by proposal type)
        voteType:
          type: string
          enum:
          - choice
          - multi-choice
          - budget
          - weighted
          - ranked
          - scale
          - likert
          description: 'Type of voting system for this proposal:

            - "choice": Single selection. 2 options maps to Hydra method "binary"; ≥3 options maps to "single-choice".

            - "multi-choice": Pick min..max options. See minSelections/maxSelections.

            - "budget": Knapsack — pick a subset whose Σ option.cost ≤ voterBudget. Backend validates cost cap.

            - "weighted": Distribute voterBudget integer points across options; Σ value must equal voterBudget exactly.

            - "ranked": Voters rank options in order of preference.

            - "scale": Numeric scale voting (e.g., 1-10) with configurable increment.

            - "likert": Rate each option independently on a 1-5 (or custom) grid.

            '
        minSelections:
          type: number
          nullable: true
          description: Lower count bound for voteType "multi-choice" (default 1). Ignored for other types.
        maxSelections:
          type: number
          nullable: true
          description: Upper count bound for voteType "multi-choice" (defaults to voteOptions.length). Ignored for other types.
        voteIncrement:
          type: number
          description: Increment value for scale votes (e.g., 1 for integer scale, 0.5 for half-point scale)
        voterBudget:
          type: number
          description: Budget limit for voters in budget vote type (total cost of selected options cannot exceed this)
        requireAnswer:
          type: boolean
          default: false
          description: 'When true, voters must submit a selection — abstain is rejected. Default false (permissive): abstain is allowed.'
        voteOptions:
          type: array
          items:
            type: object
            required:
            - id
            - label
            properties:
              id:
                oneOf:
                - type: integer
                - type: string
                  enum:
                  - abstain
                description: 'Integer option id. The string "abstain" is a legacy sentinel retained for back-compat only; production-authored ballots use the top-level `abstain: true` vote flag instead.'
              label:
                type: string
                maxLength: 120
                description: Display label for this vote option (e.g., "Yes", "No", "Strongly Agree").
              cost:
                type: number
                minimum: 0
                default: 1
                description: Per-option cost; only meaningful for voteType "budget". Defaults to 1.
              description:
                type: string
                maxLength: 1000
                description: Optional voter-facing blurb rendered alongside the label.
              referenceUrl:
                type: string
                maxLength: 500
                description: Optional canonical "learn more" link for this specific option.
              imageUrl:
                type: string
                maxLength: 500
                description: Optional thumbnail / portrait URL (e.g. candidate headshot).
              metadata:
                type: object
                description: Free-form one-off attributes the frontend understands (e.g. candidate platform). No schema contract.
          description: Array of available voting options for this proposal.
        commentCount:
          type: number
          description: Number of comments posted on this proposal (computed field)
        voteCount:
          type: number
          description: Number of unique voters who have submitted votes on this proposal (computed field)
        votingPower:
          type: number
          description: Total voting power of all votes cast on this proposal (computed field, only in weighted ballots)
        result:
          type: object
          nullable: true
          description: Calculated voting results object (structure varies by voteType, null if results not yet calculated)
        updatedAt:
          type: string
          format: date-time
          nullable: true
          description: ISO 8601 timestamp when the proposal was last updated (or when results were last calculated)
        createdAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the proposal was created
    Vote:
      type: object
      description: 'Represents a vote cast by a voter for a specific proposal within a ballot. Votes can exist

        in two states: pending (not yet submitted) and submitted. When votes are submitted via

        transaction, the submittedVote and submittedAt fields are updated. If a voter changes their

        vote before submission, only the vote field changes. Each voter can only have one vote per proposal.

        '
      properties:
        _id:
          type: string
          description: Unique identifier for the vote record
        userId:
          type: string
          description: ID of the voter who cast the vote (references Voter, format depends on voterType)
        ballotId:
          type: string
          description: ID of the ballot this vote belongs to
        proposalId:
          type: string
          description: ID of the proposal being voted on
        vote:
          type: array
          items:
            oneOf:
            - type: number
            - type: string
              enum:
              - abstain
          description: 'Current value of the vote (may differ from submittedVote if changed after initial submission).

            Array of vote option IDs (numbers) or "abstain" string. For scale votes, contains the numeric value.

            For budget votes, contains multiple option IDs that fit within the voter''s budget.

            '
        submittedVote:
          type: array
          items:
            oneOf:
            - type: number
            - type: string
              enum:
              - abstain
          nullable: true
          description: 'Value of the vote when it was last submitted via transaction. This is the vote that counts

            toward results. Null if the vote has never been submitted. Array of vote option IDs that

            were submitted (numbers) or "abstain" string.

            '
        submittedAt:
          type: string
          format: date-time
          nullable: true
          description: ISO 8601 timestamp when the vote was last submitted via transaction (null if never submitted)
        createdAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the vote was first created
        updatedAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the vote was last updated (changes when vote value is modified)
    Comment:
      type: object
      description: 'Comment on a proposal. Can be top-level (parentId null) or a reply (parentId set).

        Status is live or withdrawnByAdmin; withdrawalDetails set when withdrawn by admin.

        '
      properties:
        _id:
          type: string
          description: Unique identifier for the comment
        proposalId:
          type: string
          description: ID of the proposal this comment belongs to
        parentId:
          type: string
          nullable: true
          description: ID of parent comment for replies, or null for top-level
        userId:
          type: string
          description: ID of the user who created the comment
        content:
          type: string
          maxLength: 2000
          description: Comment content
        status:
          type: string
          enum:
          - live
          - withdrawnByAdmin
          description: Comment visibility status
        withdrawalDetails:
          type: object
          nullable: true
          description: Set when status is withdrawnByAdmin (admin only)
          properties:
            category:
              type: string
              enum:
              - Inappropriate content
              - Spam
              - Policy violation
              - Duplicate
              - Other
            userId:
              type: string
              description: Admin who withdrew
            comment:
              type: string
              description: Optional reason
            date:
              type: string
              format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CommentResponse:
      type: object
      description: Comment as returned by list/get endpoints (includes author, replyCount, likeCount, userLiked)
      properties:
        _id:
          type: string
        parentId:
          type: string
          nullable: true
        content:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        replyCount:
          type: integer
          description: Number of replies (respecting visibility)
        likeCount:
          type: integer
          description: Number of likes
        userLiked:
          type: boolean
          description: Whether the authenticated user has liked this comment
        author:
          type: object
          nullable: true
          description: User info and type (proposer, admin, drep, user)
          properties:
            _id:
              type: string
            name:
              type: string
              nullable: true
            type:
              type: string
              enum:
              - proposer
              - admin
              - drep
              - user
        withdrawalDetails:
          type: object
          description: Present only for author or admin when comment was withdrawn
    CommentLike:
      type: object
      description: A like on a comment (one per user per comment; unique on commentId + userId)
      properties:
        _id:
          type: string
        commentId:
          type: string
          description: Comment that was liked
        userId:
          type: string
          description: User who liked
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    FAQ:
      type: object
      description: 'Represents a frequently asked question in the voting system. FAQs help users understand

        how to use the platform, voting procedures, and common questions. Only FAQs with is_live=true

        are returned by the API. FAQs can be tagged for filtering and can be marked as featured

        to appear prominently on the homepage.

        '
      properties:
        _id:
          type: string
          description: Unique identifier for the FAQ
        title:
          type: string
          description: Title/question text of the FAQ
        content:
          type: string
          description: Answer content of the FAQ (supports markdown formatting)
        tags:
          type: array
          items:
            type: string
          description: Tags for categorizing the FAQ (e.g., ["voter", "proposer", "technical"])
          example:
          - voter
    Transaction:
      type: object
      description: 'Represents a transaction for submitting votes in the voting system. Transactions batch multiple

        votes together for efficient blockchain submission. Transactions track the state of vote submissions

        from initial creation through signature verification to final submission. Transactions may require

        single signatures or multiple signatures (via the multiSig array) depending on the voter type.

        The merkleRoot provides cryptographic integrity verification for all votes in the transaction.

        '
      properties:
        _id:
          type: string
          description: Unique identifier for the transaction
        userId:
          type: string
          description: ID of the voter associated with this transaction
        ballotId:
          type: string
          description: ID of the ballot this transaction belongs to
        merkleRoot:
          type: string
          description: 'Merkle root hash of all votes in this transaction. Used for cryptographic verification

            of vote integrity. This is the data that must be signed by the voter.

            '
        votes:
          type: object
          description: 'Collection of votes associated with this transaction. Keys are proposal IDs, values contain

            the vote data for each proposal. Structure: { [proposalId]: { proposalId: string, vote: number[] } }

            '
          additionalProperties:
            type: object
            properties:
              proposalId:
                type: string
                description: ID of the proposal being voted on
              vote:
                type: array
                items:
                  oneOf:
                  - type: number
                  - type: string
                    enum:
                    - abstain
                description: Array of vote option IDs (numbers) or "abstain" string for this proposal
        txHash:
          type: string
          nullable: true
          description: Blockchain transaction hash after successful submission (null if not yet submitted to blockchain)
        status:
          type: string
          enum:
          - created
          - pending
          - submitted
          description: 'Current status of the transaction:

            - "created": Transaction created but not yet signed

            - "pending": Transaction signed but multisig incomplete (for multisig transactions)

            - "submitted": Transaction fully signed and votes have been submitted

            '
        signature:
          $ref: '#/components/schemas/Signature'
          nullable: true
          description: Signature object for single-signature transactions (null for multisig transactions)
        multiSig:
          type: array
          items:
            $ref: '#/components/schemas/Signature'
          description: Array of signatures for multisig transactions (empty for single-signature transactions)
        createdAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the transaction was created
        updatedAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the transaction was last updated
    TransactionResponse:
      type: object
      description: 'Response object returned when requesting checkout for a ballot. Contains the transaction data

        that needs to be signed by the voter, along with hex-encoded values for signing operations.

        This is used in the checkout flow where voters sign their votes before final submission.

        '
      properties:
        _id:
          type: string
          description: Unique identifier for the transaction
        userId:
          type: string
          description: ID of the voter associated with this transaction
        ballotId:
          type: string
          description: ID of the ballot this transaction belongs to
        merkleRoot:
          type: string
          description: Merkle root hash of all votes in this transaction
        votes:
          type: object
          description: Collection of votes associated with this transaction (same structure as Transaction.votes)
        dataHex:
          type: string
          description: Hex-encoded merkle root for signing (this is what the voter signs)
        userIdHex:
          type: string
          description: Hex-encoded voter ID (used for signature verification)
        calidusID:
          type: string
          nullable: true
          description: Calidus ID for pool signers (only present when signType is "pool")
    Signature:
      type: object
      description: 'Signature object structure for verifying voter identity and vote integrity. The structure

        varies by signature type (drep, stake, pool) and includes cryptographic proof that the voter

        signed the transaction data. For multisig transactions, multiple signature objects are stored

        in an array until all required signatures are collected.

        '
      additionalProperties: true
    Pagination:
      type: object
      description: 'Pagination metadata included in paginated API responses. Provides information about the

        total number of items, current page, items per page, and total number of pages available.

        '
      properties:
        total:
          type: integer
          description: Total number of items matching the query (across all pages)
        page:
          type: integer
          description: Current page number (1-indexed)
        limit:
          type: integer
          description: Number of items per page (maximum 100)
        totalPages:
          type: integer
          description: Total number of pages available (calculated as ceil(total / limit))
  responses:
    BadRequest:
      description: Bad request - invalid parameters or data
      content:
        application/json:
          schema:
            type: object
            properties:
              status:
                type: string
                example: error
              message:
                type: string
    Unauthorized:
      description: Unauthorized - authentication required or invalid token
      content:
        application/json:
          schema:
            type: object
            properties:
              status:
                type: string
                example: error
              message:
                type: string
                example: Unauthorized
    Forbidden:
      description: Forbidden - insufficient permissions
      content:
        application/json:
          schema:
            type: object
            properties:
              status:
                type: string
                example: error
              message:
                type: string
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            type: object
            properties:
              status:
                type: string
                example: error
              message:
                type: string
    ServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            type: object
            properties:
              status:
                type: string
                example: error
              message:
                type: string
