Local checks
Requirements: Node.js 20 or newer.
npm ci
npm run checknpm 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:verifynpm 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:8788The 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
- Run
npm run check. - Review pending migrations and pick the right command. Do not reach for the broad one by default.
- Deploy
dist/and the Pages Functions through the configured Pages project. - Deploy the scheduler Worker separately if it changed.
- 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 migrateapplies everything pendingThat 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:
0021requires exactly zeroprogram_eventsrows in production, and must run after0018and0020and before the first event import.0019is historical schema for an authorization rehearsal that never became Calendar authority. Do not apply it as part of a feature deployment.0015has a documented pre-apply sequence: pause the sole renewal caller, freeze and verify the retrospective cohort, then follow the suppression and verification steps.0017plus a distinctMANAGE_LINK_SECRETare both required before/managecan send email. The route fails closed without either.
Configuration map
| Area | Names |
|---|---|
| Storage | SUBS_DB, CAL_BUCKET, PROGRAMS_ICS_KEY, plus separately commissioned resident, WhatsApp, and retained-knowledge R2 bindings |
| Origins | BASE_PUBLIC, PROGRAMS_FEED_ORIGIN |
RESEND_API_KEY, RESEND_FROM, RESEND_WEBHOOK_SECRET, RENEWAL_EMAILS_ENABLED | |
| Bot check | TURNSTILE_SITE_KEY, TURNSTILE_SECRET |
| Tokens and signing | TOKEN_TTL_DAYS, TOKEN_PEPPER, MANAGE_LINK_SECRET |
| Admin identity | ACCESS_TEAM_DOMAIN, ACCESS_AUD |
| Admin authorization | D1 calendar_admin_grants; CALENDAR_ADMIN_EMAILS for immutable recovery; CALENDAR_COMMS_EDITOR_EMAILS transition only |
| Read-only view-as | CALENDAR_VIEW_AS_HUMAN_EMAILS, optional CALENDAR_VIEW_AS_NON_HUMAN_EMAILS, and TOKEN_PEPPER |
| Cron | CRON_BEARER_TOKEN |
| Happenings intake | CALENDAR_EVENT_INTAKE_ENVIRONMENT, HAPPENINGS_CALENDAR_DELIVERY_CREDENTIAL |
| WhatsApp intake | WHATSAPP_CALENDAR_DELIVERY_CREDENTIAL, private-message credential/bucket after commissioning, and bounded group-admin origin/credential |
| Pub projection | CALENDAR_PUB_TV_ENVIRONMENT, PUB_TV_CALENDAR_READ_CREDENTIAL |
| Retiring compatibility | HOUSE_SYSTEMS_SUMMARY_CREDENTIAL only while the legacy aggregate route remains commissioned |
| Legacy Internal feed | N8N_BASE |
| Emergency only | PROGRAMS_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:
| Command | Purpose |
|---|---|
operator:event-log-retention-report | Aggregate-only retention report |
operator:redact-access-log-paths | Resumable, ceiling-bounded cleanup of legacy query-bearing access-log rows |
operator:redact-resend-webhook-payloads | Minimize legacy provider payloads |
operator:reconcile-resend-webhook-links | Reconcile early webhook rows to their subjects |
operator:winback-suppression | Commission the win-back suppression cohort |
Common failure checks
A resident did not receive a magic link
- Check
RESEND_API_KEYandRESEND_FROM. - Inspect the request event and the matching
email_eventsrow. - Confirm the 60-second per-email cooldown is not active.
- 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
- Confirm the token is current, not revoked, and not expired.
- Confirm the subscriber and the Programs calendar are both enabled.
- Confirm
programs.icsexists inCAL_BUCKET. - Inspect
calendar_access_logwithout 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
- Check Programs scheduler freshness in System health. It flags stale after 45 minutes, which is three missed slots.
- Read the outcome code.
programs_unchangedis a healthy result and writes nothing. - Confirm the expected events actually have
review_status = published. - Confirm the scheduler’s immutable environment row exists; without it every run fails closed.
- 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
- 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.
- A receipt with
parseStatus: failedmeans extraction failed. The receipt exists precisely so this is visible. - A receipt with zero candidates and
parsedmeans the message genuinely contained nothing extractable. - 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
| Status | Check |
|---|---|
401 | The Access application, JWT issuer, and audience |
403 | Whether the verified email has a recovery or D1 grant and whether that exact snapshot contains the method/path permission |
503 | Whether 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
- Confirm
POST /api/admin/templates/previewmaps tocalendar.template.read. - Confirm the template migrations are applied.
- Compare the preview with a test send; both use the same renderer.
- Use version history to restore a bad template or footer edit rather than editing D1 directly.
Observability
calendar_access_logexplains feed delivery status and latency.event_logpowers product and security timelines, funnel, cohort, and replay.email_eventsrecords signed provider delivery outcomes.calendar_scheduler_runsrecords every rebuild slot and its outcome.system_staterecords cron summaries.happenings_content_intakesrecords every accepted message, including empty and failed parses.