Use this runbook for the voting Worker and its D1 database. Read Voting system before changing ballot behavior or token handling.
Runtime
| Part | Current implementation |
|---|---|
| Application | Cloudflare Worker at src/index.ts, with routes under src/routes/ |
| Static files | public/, served through the Worker assets binding |
| Primary data | D1 database bound as DB |
| Token coordination | TokenManager Durable Object bound as TOKEN_MANAGER |
| Scheduled work | Cron trigger every minute |
| Outbound email | Resend calls from src/utils/email.ts |
| Observability | Worker observability enabled in wrangler.jsonc |
Local development
From the ihnyc-rc-vote repository:
npm ci
cp .dev.vars.example .dev.vars
npm run devAdd local values to .dev.vars; never commit that file. The checked-in example documents every supported value.
| Value | Used for |
|---|---|
RESEND_API_KEY | Invitation, reminder, and results email |
FROM_EMAIL | Verified sender identity |
BASE_URL | Links back to the deployed voting service |
ADMIN_API_KEY | Optional API-key administration when Cloudflare Access is not used |
REMINDER_DEFAULT_HOURS_BEFORE_CLOSE | Default reminder window |
REMINDER_MAX_EMAILS_PER_RUN | Per-cron reminder safety cap |
NOTION_API_KEY | Optional election metadata sync |
NOTION_DATABASE_ALLOWLIST | Notion databases the admin may link |
Generate current Worker binding types after changing wrangler.jsonc:
npm run cf-typegenTests
npm test
npm run test:unit
npm run test:integrationThe full suite uses Vitest and @cloudflare/vitest-pool-workers. See Testing for its runtime, mocks, and known schema gap.
Database migrations
Migrations are ordered SQL files in migrations/.
# Local database
npx wrangler d1 migrations apply ihnyc-rc-vote
# Production database
npx wrangler d1 migrations apply ihnyc-rc-vote --remoteCheck the production migration list first
Take a current Time Travel bookmark, apply pending migrations in order, and verify the affected tables before deploying code that depends on them. Several migrations rebuild
electionswhile preserving dependent rows.
Inspect the remote schema when a migration or route reports a missing column:
npx wrangler d1 execute ihnyc-rc-vote --remote \
--command="SELECT name, sql FROM sqlite_master WHERE type='table';"The migration reference lists files 001 through 016, rebuild behavior, and focused checks.
Deploy
- Run
npm test. - Confirm required remote migrations are applied.
- Set each production secret with
npx wrangler secret put NAME. - Deploy with:
npm run deployAfter deployment:
- Open the public service and load an election that is safe to inspect.
- Open the protected admin surface through its normal access path.
- Confirm the scheduled handler and email paths show no new errors in Worker logs.
- Run the focused database checks from the migration that shipped.
Do not submit a production ballot as a deployment probe. Use the automated suite and a test election.
Back up and restore
Cloudflare D1 Time Travel is automatic. Its current retention is 30 days on Workers Paid plans and 7 days on Free plans.
# Record the current bookmark
npx wrangler d1 time-travel info ihnyc-rc-vote
# Find the bookmark for an RFC 3339 timestamp
npx wrangler d1 time-travel info ihnyc-rc-vote \
--timestamp="2026-01-03T20:20:00+00:00"A restore overwrites the remote database
Confirm the database, timestamp, and bookmark before continuing. Keep the previous bookmark printed by Wrangler so the restore can be undone.
npx wrangler d1 time-travel restore ihnyc-rc-vote --bookmark=BOOKMARKSee Cloudflare’s Time Travel reference for current limits and restore behavior.
Failure checks
| Symptom | Check first |
|---|---|
| Ballot request is rejected | Election window, ballot type, token state, and the current constraints in Voting system |
| Invitation or reminder did not send | Worker logs, Resend configuration, invite status, and Email system |
| Results appear stale | Election visibility, manual-results state, and the cache behavior in Results calculation |
| Notion metadata does not sync | NOTION_API_KEY, the allowlist, page properties, and Notion setup |
| A route reports a missing table or column | Remote migration list and the migration reference |