The voting Worker writes selected activity to the D1 audit_logs table. These records support incident review and troubleshooting; they are not a complete request log.

Recorded actions

ActionWritten when
election.createdAn admin creates an election
election.deletedAn admin deletes an election
election.closedAn admin closes an election manually
election.viewedThe public election detail route is opened
tokens.generatedAn admin creates a token batch
vote.submittedA ballot is accepted
results.viewedThe legacy results route is requested
token.invalidA submitted ballot token fails validation
admin.accessAn invite is resent, bulk reminders or results email run, election dates change, or a distribution list is mutated

rate_limit.exceeded exists in the action constants but the current rate-limit middleware does not write it.

Record shape

ColumnTypeNotes
idtextGenerated UUID
actiontextOne of the action strings above
ip_addresstext or nullCF-Connecting-IP, then X-Forwarded-For
user_agenttext or nullRequest User-Agent
election_idtext or nullForeign key to elections; set to null when the election is deleted
timestampintegerUnix time in milliseconds
metadatatextJSON object serialized as text

Indexes cover action, election_id, and timestamp.

Example:

{
  "id": "4d502067-4ca3-4d61-b431-3d3ccbf74a75",
  "action": "vote.submitted",
  "ip_address": "192.0.2.1",
  "user_agent": "Mozilla/5.0",
  "election_id": "elec_123",
  "timestamp": 1785038400000,
  "metadata": "{\"ballot_type\":\"SIMPLE_TRIPLE\",\"choice\":\"YES\"}"
}

Plaintext ballot tokens and admin API keys are not written by the current audit call sites. Some admin.access metadata does contain operational data, including an invite email address when an invitation is resent.

Viewing records

GET /admin/audit-logs renders the authenticated browser interface. It supports:

Query parameterPurposeDefault
limitRows per page100
offsetPagination offset0
actionExact action filternone
election_idExact election filternone

Current JSON-route conflict

A JSON handler is registered at the same path, but the admin UI router is mounted first. Requests currently reach the HTML page, not the JSON handler. Route separation is tracked in ihnyc-rc-vote issue #10.

For machine-readable access in the current release, query D1 directly:

SELECT *
FROM audit_logs
WHERE election_id = ?
ORDER BY timestamp DESC;

Failure and retention behavior

Audit writes are best effort. createAuditLog catches insertion errors, writes them to the Worker console, and lets the original request continue.

There is no automatic retention, archival, export, or alerting. Records remain in D1 until they are removed operationally.

Sources

Verified against ihnyc-rc-vote commit 2451aa1.

  • migrations/002_audit_logs.sql
  • src/utils/audit.ts
  • src/routes/admin.ts
  • src/routes/admin-ui.ts
  • src/routes/vote.ts
  • src/routes/results.ts
  • src/index.ts