Cron jobs and ops
What runs on a schedule, where it's defined, how to authenticate manual runs, and how to hand the schedule off to an ops team.
Vercel's built-in cron is the authoritative scheduler. The schedule lives in vercel.json at the repo root and runs automatically on every production deployment. Ops doesn't need to configure anything to keep the schedule alive on a Vercel deployment.
The schedule
| Job | Endpoint | Schedule | Max duration | What happens if it misses |
|---|---|---|---|---|
| Event reminders | POST /api/cron/event-reminders | Every 1 min | ~30 s | Users miss the 15-minute heads-up push for upcoming meetings |
| Activity watches | POST /api/cron/watch-poll | Every 5 min | ~60 s | Users stop getting "new comment / new file" push notifications |
| Incremental sync | POST /api/sync/cron | Every 15 min (work hours), hourly (off-hours) | ~30 s typical, up to 5 min | Calendar shows stale data |
| Full sync | POST /api/sync/cron?mode=full | Once daily, off-peak | ~2–5 min | Cache drift accumulates; mostly harmless short-term |
| Recurrence materialize | POST /api/cron/recurrence-materialize | Once daily, off-peak | ~30 s | New occurrences of repeat tasks/events don't appear |
All five jobs are idempotent. Running one twice in the same minute is safe.
vercel.json is the source of truth. Times are UTC. Edit that file to change any schedule.
Authentication
Every cron route requires a Bearer token:
Authorization: Bearer <CRON_SECRET>
Set CRON_SECRET in Vercel → Project → Settings → Environment Variables. Vercel injects this header automatically when it fires your scheduled routes. Manual callers (curl, the shell script) must add it themselves.
Without the header the endpoint returns 401. If CRON_SECRET is missing from the server env entirely the endpoint returns 500 — it refuses to fail open.
Manual runs
scripts/herbe-cron.sh is a curl wrapper for manual invocations. Use it to:
- Dry-run a job to see what it would do (
dryRun=1support on event-reminders and watch-poll). - Force a sync outside the schedule — useful after restoring a database or fixing a stuck connection.
- Run jobs from an ops host as an emergency fallback if Vercel cron is ever disabled.
Setup:
export HERBE_BASE_URL=https://calendar.herbe.app
export HERBE_CRON_SECRET=<value from Vercel env>
Usage:
scripts/herbe-cron.sh event-reminders dryrun
scripts/herbe-cron.sh watch-poll
scripts/herbe-cron.sh sync-incremental
scripts/herbe-cron.sh sync-full
scripts/herbe-cron.sh recurrence-materialize
Or directly via curl:
curl -fsS -X POST \
-H "Authorization: Bearer $HERBE_CRON_SECRET" \
"https://calendar.herbe.app/api/cron/event-reminders?dryRun=1"
Emergency fallback crontab
If Vercel cron must be taken offline, drop the crons block from vercel.json and install this crontab on any Linux host with the script and env vars configured:
HERBE_BASE_URL=https://calendar.herbe.app
HERBE_CRON_SECRET=<prod secret>
LOG=/var/log/herbe-cron.log
* * * * * /usr/local/bin/herbe-cron.sh event-reminders >> $LOG 2>&1
*/5 * * * * /usr/local/bin/herbe-cron.sh watch-poll >> $LOG 2>&1
*/15 6-22 * * * /usr/local/bin/herbe-cron.sh sync-incremental >> $LOG 2>&1
0 0-5,23 * * * /usr/local/bin/herbe-cron.sh sync-incremental >> $LOG 2>&1
0 3 * * * /usr/local/bin/herbe-cron.sh sync-full >> $LOG 2>&1
30 3 * * * /usr/local/bin/herbe-cron.sh recurrence-materialize >> $LOG 2>&1
Monitoring
Check Vercel's Cron tab for green ticks after each scheduled window. For structured log inspection:
vercel logs https://calendar.herbe.app --json \
| grep -oE '"requestPath":"/api/(cron|sync)/[^"]+"' \
| sort | uniq -c
A passing run prints OK <job>: {...} to stdout. A failing run exits non-zero and prints to stderr. If you ship logs to a log aggregator, grep for ^[^ ]+ FAIL for alertable lines.
Handing off to an ops team
Share scripts/CRON-HANDOFF.md with whoever runs the ops host. It contains the required env vars, the crontab block, and the expected response shape for each endpoint so they know what "healthy" looks like without reading source.