Scope
Web goes first for the next migration step: its five Vercel schedules move to the existingapps/cron-control Worker. This change prepares dormant scheduling and
keeps the existing authenticated Next.js processors and Supabase job state.
It does not change canonical web traffic, deploy a Worker, enable a job, or remove
any Vercel cron. The paused apps/tanstack-web runtime stays paused.
Cron migration
CRON_CONTROL_WEB_MODE defaults to off, in addition to the existing per-job
CRON_CONTROL_ENABLED_JOBS allowlist. shadow reports due selected jobs as
skipped with reason shadow; it does no ledger, database or processor work.
active permits one authenticated HTTP attempt per job and slot, with the
existing timeout and five-failed-slot circuit breaker. There are no web retries,
Queue redeliveries or self-rearming alarms. Vercel entries remain unchanged.
The Worker uses WEB_ORIGIN and secret CRON_SECRET_WEB, which matches web’s
existing CRON_SECRET. Provision secrets only during an authorized rollout;
this change does not add them to the deployment workflow. Requests include the
scheduled slot in X-Tuturuuu-Cron-Slot, the existing user agent, and a bearer
token. Redirects are refused. The slot alone grants no authority.
The web fence compares the bearer token without a content-dependent early exit,
using the same byte comparison as Cron Control’s operator endpoint. It preserves
each handler’s secret selection: post-email, report automation and topic
announcements fall back to VERCEL_CRON_SECRET only when CRON_SECRET is absent.
Notification batches and deadline reminders require CRON_SECRET. Configure
CRON_SECRET_WEB to the selected secret; an empty CRON_SECRET does not activate
the fallback.
Processor timeout budgets
Both 165-second processors need the long timeout; neither fits the 120-second
default. The remaining three jobs have work-count bounds, not proof that every
invocation finishes within 120 seconds. Before enabling them, measure production
database/provider latency with representative fan-out and verify completion
within the dispatch window. A dispatch timeout cannot cancel server-side work;
it leaves an unknown outcome and the shared gate retains the original operation
for reconciliation. Each gate call has a separate 10-second deadline. Catalog
timeouts and the additional 30-second dispatch lease margin are covered by
apps/cron-control/src/web-crons.test.ts; secret precedence, per-job fallback and
invalid bearers are covered by apps/web/src/lib/cron/web-cron-fence.test.ts.
Shared write lease and durable stop
Web’s five GET processors usewithWebCronFence before job work. Without
WEB_CRON_GATE_ORIGIN, Vercel retains its current behavior and Cloudflare
requests are refused. Once configured, both schedulers must acquire the
same per-job SQLite execution gate in Cron Control’s existing CronLedger.
Set CRON_CONTROL_WEB_GATE_TOKEN on web and Cron Control to the same separately
provisioned secret, at least 32 characters. The Worker reads this optional secret
only for /web-crons/<job-id>; it is intentionally not a required deployment
binding while the migration is dormant. Use an HTTPS WEB_CRON_GATE_ORIGIN.
Gate calls have a 10-second deadline and no retry.
Each gate defaults to owner vercel. It persists the last admitted slot and an
active operation id before the processor runs. Concurrent requests, duplicate
or older slots, stopped gates and requests from the other owner cannot write.
The web server generates a fresh operation id once and uses it for completion.
A failed request or lost acknowledgement retains an unresolved operation; lease
expiry never grants replay permission. A successful completion releases the
active run but preserves the slot. Completed slots survive owner changes and
Worker restarts. No database migration or production write is part of preparation.
Operator actions use CRON_CONTROL_OPERATOR_TOKEN and POST JSON to
/control/web-crons/<job-id>:
Stop prevents new processor starts; it cannot retract an email or cancel work
already admitted. A normal in-flight run can complete while stopped. Before
resolve, confirm the old invocation has ended and reconcile its original job
claims and provider outcome. Resolution is an explicit operator acknowledgement,
not an automatic timeout retry. Never clear an unknown email send just to unlock
a job. Keep both the gate and dispatch ledger stopped until reconciliation ends.
Work and operation bounds
Cloudflare invokes one existing processor per selected job per slot, at most four concurrently. A successful guarded invocation makes two gate calls (acquire and complete); a rejected invocation makes one. The scheduled ledger adds one claim and one completion write per admitted dispatch. A stopped or duplicate gate makes no gate-state write. Disabled and shadow web jobs make zero gate/processor calls. Repeated failure stops dispatch after five slots, including across restarts. These bounds do not claim a dollar saving or bound hosted ingress abuse. Cloudflare requests also have an invocation-scoped work budget. Notification batches select at most 25 pending batches; shared notification pagination permits 20 page reads per invocation and throws before the next read. Remaining pending batches keep their existing status for the next scheduled slot. Deadline reminders allow 20 combined settings/task page reads, at most 500 tasks and 1,000 watcher entries, checked before reminder creation. Supported intervals are deduplicated by the existing processor. Exceeding a bound fails visibly and retains the active operation for investigation; it does not silently truncate reminders. Vercel’s current pagination behavior is unchanged. Post-email fallback pagination shares the 20-page budget. Post emails retain the existing clamped drain/send limits and 165-second processing budget; topic announcements retain their 25-row claim limit; report automation reconciles at most 25 due schedules per invocation (later schedules stay due), with 1,000-entry catalog and group guards, and retains its eight-run claim and finite email-drain batches. Their business claims, provider reconciliation and terminal statuses remain authoritative. The shared execution gate supplies exclusive scheduling for deadline reminders, whose receipt check and notification creation are still separate RPCs. For Cloudflare, a creation error, missing notification id or receipt persistence error fails the invocation and retains its original execution lease for reconciliation. Do not remove the gate or enable another reminder scheduler until those writes have an atomic claim.Authorized handover and rollback
- Deploy the dormant Worker and the five guarded web adapters under a separate rollout authorization. Configure the shared gate secret and HTTPS origin on web while the gate owner remains Vercel. Drain invocations from the earlier unguarded deployment for at least the platform’s maximum invocation lifetime. Verify Vercel acquires/completes leases and that a duplicate slot is refused.
- Select the five ids and set mode
shadow. Compare the due job ids and slots against Vercel execution logs over each cadence, including the 30-minute job. Shadow compares scheduling only; processor result parity needs isolated fixtures and later authorized active acceptance. Confirm no Cloudflare processor writes. - For one job at a time, stop its execution gate and its dispatch ledger via
/control/jobs/<job-id>/stop. Drain/reconcile its active operation. Verifyactive: null; change gate owner tocloudflare. Keep the durable gate wired on web throughout. Vercel now fails the owner check even while still scheduled. - Resume the execution gate, set mode
activeand resume that job’s dispatch ledger. Observe one terminal processor result and duplicate rejection. Only after that acceptance, disable that Vercel cron in a separately reviewed change and shrink the backend ownership allowlist. Do not runsync-web-cronsagainst the stale retired Docker catalog. - To roll back, stop both fences, drain/reconcile the active operation, change
gate owner to
vercel, turn Cloudflare mode off (or deselect the job), restore the Vercel schedule if removed, then resume the gate. Keep its origin and secret configured. Clearing the gate environment is not a safe rollback.
Regression evidence
apps/cron-control/src/web-crons.test.ts pins Vercel/catalog/adapter schedule
parity, dormant/shadow operation counts, duplicate dispatch, failure budgets and
stop races. web-gate.test.ts covers owner exclusion, retained UNKNOWN operations,
reconciliation identity and durable stop decisions. The existing Worker runtime
harness now repeats lease, stop, owner and duplicate checks across real workerd
restarts backed by local SQLite. Web’s web-cron-fence.test.ts covers admission,
authentication, network/completion failure and work-budget boundaries. Run Worker
types, focused tests and bun check:changed on an admitted validation host.