Source
x-pcc-internal-source names the caller as scheduler-worker.
How the scheduler starts seven API jobs without running two copies at once.
Every thirty minutes, the Scheduler Worker sends seven private requests to the API at the same time. It records each result and then stops.
The scheduler starts the jobs. The API decides what changes. The Discord Worker handles Discord updates.
System flow
Every thirty minutes, the Scheduler Worker starts seven private API jobs at the same time. The API claims a D1 lease and decides what each job changes.
Start each job
Seven jobs
| Job | Private API path | What it does |
|---|---|---|
| Expire memberships | /internal/jobs/memberships/expire | Find expired memberships, update access, and start any role sync. |
| Reconcile membership roles | /internal/jobs/memberships/roles/reconcile | Update Discord roles from receipt membership data. |
| Update the darkroom schedule | /internal/jobs/darkroom/schedule/sweep | Check the schedule, apply timed changes, and update Discord messages. |
| Update the studio schedule | /internal/jobs/studio/schedule/sweep | Move the studio schedule forward and publish the changes. |
| Update darkroom stats | /internal/jobs/darkroom/stats/sync | Recalculate the latest stats and update them in Discord. |
| Send equipment reminders | /internal/jobs/equipment/reminders/run | Find reminders that are due, claim each send, and notify the right member or staff member. |
| Expire photographer requests | /internal/jobs/photographer-requests/expire | Expire old requests and update their public or Discord status. |
The seven paths are fixed in the scheduler source. Adding another job requires a code change and a new deployment. A database row cannot tell the Worker to call another internal URL.
Cloudflare Cron calls the Worker’s scheduled handler with the cron string and scheduled timestamp. The Worker checks its INTERNAL_TOKEN and API_WORKER binding, then sends all seven requests with Promise.all.
The jobs do not depend on each other, so they can run at the same time. Darkroom stats do not need to wait for membership expiry, and equipment reminders do not hold up the studio schedule.
Each result includes:
The final summary counts the jobs that passed and failed. If one job fails, the other six results are still kept.
Each request is a POST to an internal URL. It includes three headers:
Source
x-pcc-internal-source names the caller as scheduler-worker.
Token
x-internal-token must match the shared secret in the API.
Scheduled time
x-pcc-scheduled-at holds the original Cron timestamp.
The source header is not proof by itself because a browser can make the same header. The internal token and service binding prove that the request came from a trusted service.
The API handles these job paths before normal browser session checks. They are private service endpoints, not staff dashboard routes or public Cron URLs.
Cron may send the same trigger again after a delay. A deployment may also overlap with a scheduled run. Before a job starts, the API claims a D1 lease for that job.
A lease records:
If another request arrives while the lease is active, the API does not run that job again. When the job ends, the API records success or failure and updates the lease state.
The scheduler sends the request. The API decides whether the job can run.
A lease stops two copies of a job from running at once. The job’s code must also make each record update idempotent.
For example, an equipment job must know which reminder is due before it sends a Discord event. A membership job must update only expired memberships. A photographer request job must update only requests that can expire.
The lease protects the whole job. The job’s rules protect each record.
Some jobs change messages, roles, or other items in Discord. The scheduler never calls Discord REST and does not have a Discord token.
The request follows this path:
Scheduled and manual changes use the same event contract.
The Worker’s health response shows its service name and configured job count. The health route is rate limited.
The scheduler configuration sets a limit of ten requests per minute. The code uses this limit only for public GET / and GET /health requests. The scheduled handler does not use it. Cron runs through Cloudflare’s scheduled trigger, and the API requests use the private binding and internal token.
There are two kinds of failure.
An API call can return an error status or fail before it gets a response. The Worker saves that result and waits for the other jobs. The summary logs how many passed and failed.
The API can claim a lease and then fail while running the job. The job state records the failure, the API logs the job details, and the Scheduler Worker receives an error response.
A Cron trigger can succeed while one of the seven jobs fails. The summary keeps a separate result for each path.
The Email Worker’s five-minute Cron is separate from these seven jobs.
Every five minutes, it checks the receipt KV namespace for intake records that can be retried. Email parsing and delivery recovery stay in the Email Worker. The thirty-minute Scheduler Worker runs the club workflows owned by the API.