Local checks

Requirements: Node.js 20 or newer.

npm ci
npm run check

npm run check is the full gate. It runs, in order: the ServiceSnapshot contract verification, the test suite, the scheduler type-check, the Eleventy build, the admin build-output verification, and a dry-run build of the scheduler Worker.

Individual steps when iterating:

npm test                   # vitest
npm run build              # Eleventy -> dist/
npm run build:verify-admin # every admin destination emitted at its stable path
npm run typecheck:scheduler
npm run build:scheduler
npm run contract:verify

npm run dev serves Eleventy on port 8080. To exercise Pages Functions, build first and override both origins explicitly:

npx wrangler pages dev dist --port 8788 \
  --binding BASE_PUBLIC=http://localhost:8788 \
  --binding PROGRAMS_FEED_ORIGIN=http://localhost:8788

The origin validators accept loopback HTTP for local development and require HTTPS everywhere else. Neither value is ever derived from the request host. Working subscription, admin, cron, intake, and feed paths also need suitable local D1 and R2 bindings and non-production credentials.

Deployment order

  1. Run npm run check.
  2. Review pending migrations and pick the right command. Do not reach for the broad one by default.
  3. Deploy dist/ and the Pages Functions through the configured Pages project.
  4. Deploy the scheduler Worker separately if it changed.
  5. Smoke-test the resident form, one tokenized feed request, the admin authorization boundary, one rendered email preview, /updates, and any newly commissioned Operations destination.

Migrations

npm run migrate applies everything pending

That includes unrelated maintenance work. Use the one-purpose commands and read the list output before applying.

The repository exposes paired migration:<feature>:list and migration:<feature>:apply scripts for bounded features, including scheduler, access-log redaction, Programs triage/assets/operations/source batches, forwarders, operator access/roles, WhatsApp messages, Bulletin Ask/search, official PDFs, renewal delivery, Calendar Note templates, and platform contact history.

Run the matching :list command first and confirm its exact filename. The migration directory currently contains two distinct 0034 files, so the filename and Cloudflare ledger—not the numeric prefix alone—identify a migration.

npm run migrate:local is for disposable local fixtures only.

Sequencing constraints worth knowing before an apply:

  • 0021 requires exactly zero program_events rows in production, and must run after 0018 and 0020 and before the first event import.
  • 0019 is historical schema for an authorization rehearsal that never became Calendar authority. Do not apply it as part of a feature deployment.
  • 0015 has a documented pre-apply sequence: pause the sole renewal caller, freeze and verify the retrospective cohort, then follow the suppression and verification steps.
  • 0017 plus a distinct MANAGE_LINK_SECRET are both required before /manage can send email. The route fails closed without either.

Configuration map

AreaNames
StorageSUBS_DB, CAL_BUCKET, PROGRAMS_ICS_KEY, plus separately commissioned resident, WhatsApp, and retained-knowledge R2 bindings
OriginsBASE_PUBLIC, PROGRAMS_FEED_ORIGIN
EmailRESEND_API_KEY, RESEND_FROM, RESEND_WEBHOOK_SECRET, RENEWAL_EMAILS_ENABLED
Bot checkTURNSTILE_SITE_KEY, TURNSTILE_SECRET
Tokens and signingTOKEN_TTL_DAYS, TOKEN_PEPPER, MANAGE_LINK_SECRET
Admin identityACCESS_TEAM_DOMAIN, ACCESS_AUD
Admin authorizationD1 calendar_admin_grants; CALENDAR_ADMIN_EMAILS for immutable recovery; CALENDAR_COMMS_EDITOR_EMAILS transition only
Read-only view-asCALENDAR_VIEW_AS_HUMAN_EMAILS, optional CALENDAR_VIEW_AS_NON_HUMAN_EMAILS, and TOKEN_PEPPER
CronCRON_BEARER_TOKEN
Happenings intakeCALENDAR_EVENT_INTAKE_ENVIRONMENT, HAPPENINGS_CALENDAR_DELIVERY_CREDENTIAL
WhatsApp intakeWHATSAPP_CALENDAR_DELIVERY_CREDENTIAL, private-message credential/bucket after commissioning, and bounded group-admin origin/credential
Pub projectionCALENDAR_PUB_TV_ENVIRONMENT, PUB_TV_CALENDAR_READ_CREDENTIAL
Retiring compatibilityHOUSE_SYSTEMS_SUMMARY_CREDENTIAL only while the legacy aggregate route remains commissioned
Legacy Internal feedN8N_BASE
Emergency onlyPROGRAMS_ICS_ALLOW_EMPTY_D1_FEED

Every machine credential is dedicated. Reusing an admin, cron, email, signing, or another lane’s credential fails closed by design rather than working quietly. ACCESS_TEAM_DOMAIN and ACCESS_AUD are non-secret identifiers kept in checked-in configuration on purpose, so config ships atomically with the code that reads it and there is no window where admin APIs deploy unconfigured.

Secrets belong in Cloudflare Pages secrets or a local secret environment, never in source or documentation.

Operator scripts

Each runs deliberately, not as part of a deploy:

CommandPurpose
operator:event-log-retention-reportAggregate-only retention report
operator:redact-access-log-pathsResumable, ceiling-bounded cleanup of legacy query-bearing access-log rows
operator:redact-resend-webhook-payloadsMinimize legacy provider payloads
operator:reconcile-resend-webhook-linksReconcile early webhook rows to their subjects
operator:winback-suppressionCommission the win-back suppression cohort

Common failure checks

  1. Check RESEND_API_KEY and RESEND_FROM.
  2. Inspect the request event and the matching email_events row.
  3. Confirm the 60-second per-email cooldown is not active.
  4. Confirm Turnstile verification succeeds when it is enabled.

Remember that requesting a second link does not invalidate the first within its six-hour window.

The feed returns not found

  1. Confirm the token is current, not revoked, and not expired.
  2. Confirm the subscriber and the Programs calendar are both enabled.
  3. Confirm programs.ics exists in CAL_BUCKET.
  4. Inspect calendar_access_log without copying private token values into tickets or chat.

An expired token receives the renewal nudge calendar rather than a bare failure, so “the calendar now shows one renewal reminder” is the expected symptom of expiry, not a bug.

The Programs feed is stale

  1. Check Programs scheduler freshness in System health. It flags stale after 45 minutes, which is three missed slots.
  2. Read the outcome code. programs_unchanged is a healthy result and writes nothing.
  3. Confirm the expected events actually have review_status = published.
  4. Confirm the scheduler’s immutable environment row exists; without it every run fails closed.
  5. Check for a lease that was lost or expired, which points at overlap or a deadline overrun rather than at data.

A forwarded newsletter produced nothing

  1. Look for a receipt in the content intake inbox. No receipt means the message was never admitted or never recognized, which is a routing or recognition problem upstream of Calendar.
  2. A receipt with parseStatus: failed means extraction failed. The receipt exists precisely so this is visible.
  3. A receipt with zero candidates and parsed means the message genuinely contained nothing extractable.
  4. Candidates always arrive as needs_review. Nothing publishes itself.

See What happens when you forward to happenings@.

An event will not publish

Publishing is refused with an unresolved_matches code while the event has an unresolved possible duplicate. Resolve the pair, then publish.

Admin API returns 401, 403, or 503

StatusCheck
401The Access application, JWT issuer, and audience
403Whether the verified email has a recovery or D1 grant and whether that exact snapshot contains the method/path permission
503Whether recovery configuration is malformed or the D1 authorization/role schema is unavailable or invalid

Routine assignments belong in /admin/access/; do not add ordinary operators to recovery configuration or edit grant JSON directly. Authentik/Cloudflare membership is a separate identity and edge-admission step.

Email preview is blank or stale

  1. Confirm POST /api/admin/templates/preview maps to calendar.template.read.
  2. Confirm the template migrations are applied.
  3. Compare the preview with a test send; both use the same renderer.
  4. Use version history to restore a bad template or footer edit rather than editing D1 directly.

Observability

  • calendar_access_log explains feed delivery status and latency.
  • event_log powers product and security timelines, funnel, cohort, and replay.
  • email_events records signed provider delivery outcomes.
  • calendar_scheduler_runs records every rebuild slot and its outcome.
  • system_state records cron summaries.
  • happenings_content_intakes records every accepted message, including empty and failed parses.