Cron Jobs
Scheduled tasks that run automatically on Vercel.
Cron jobs are scheduled HTTP requests that Vercel fires on a timetable. DirectoryKit ships with two.
What's scheduled
"crons": [
{ "path": "/api/cron/competitions", "schedule": "15 8 * * *" },
{ "path": "/api/cron/winner-reminders", "schedule": "0 9 * * *" }
]| Endpoint | When | What it does |
|---|---|---|
/api/cron/competitions | 08:15 UTC daily | Closes competitions that have ended, picks winners, triggers notifications |
/api/cron/winner-reminders | 09:00 UTC daily | Emails winners who haven't yet claimed their prize |
Both paths are regular Next.js API routes under app/api/cron/. You can run them manually with curl — useful for testing.

How cron URLs are protected
Anyone on the internet could hit https://your-site.com/api/cron/competitions otherwise. To prevent that, every cron endpoint checks this header:
Authorization: Bearer ${CRON_SECRET}
Vercel automatically adds the header when it calls your cron endpoints, using the CRON_SECRET env var. If CRON_SECRET is missing or wrong → the endpoint returns 401 Unauthorized.
Generating a good CRON_SECRET
openssl rand -hex 32Paste that into both .env.local and Vercel → Settings → Environment Variables.
Vercel plan limits
| Plan | Cron rate limit |
|---|---|
| Hobby (free) | 1 cron, runs up to once per day |
| Pro | Multiple crons, any schedule |
| Enterprise | No limits |
If you're on Hobby, the default config will hit the limit. Either:
- Keep one cron and disable the other
- Upgrade to Pro
Reading cron schedules
The format is standard Unix cron: min hour day month weekday. Examples:
| Expression | When |
|---|---|
0 * * * * | Every hour on the hour |
*/15 * * * * | Every 15 minutes |
0 9 * * * | 09:00 UTC daily |
0 9 * * 1 | 09:00 UTC every Monday |
0 0 1 * * | Midnight UTC on the 1st of each month |
Use crontab.guru to sanity-check.
Vercel cron schedules are UTC. Convert from your local time before scheduling.
Adding a new cron job
1. Create the endpoint
import { NextRequest } from 'next/server';
export async function GET(req: NextRequest) {
// Verify the request is from Vercel cron
const auth = req.headers.get('authorization');
if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
// Do your work
// ...
return Response.json({ ok: true });
}2. Register the schedule
Edit vercel.json:
"crons": [
{ "path": "/api/cron/my-task", "schedule": "0 */6 * * *" }
]3. Deploy
Vercel reads vercel.json on deploy and wires the schedule. Changes to schedules require a redeploy.
Add a new cron job to this DirectoryKit project.
Details:
- Endpoint path: /api/cron/{your-task}
- What it does: {1 sentence — e.g. "expires promotions whose end_date has passed and refunds unused time"}
- Schedule: {e.g. "every day at 3am UTC" or give a cron expression}
- What it queries / writes: {tables and fields}
- What it emails or notifies (if any): {recipients, type}
Please:
- Create app/api/cron/{your-task}/route.ts following the existing pattern in app/api/cron/.
- Enforce
Authorization: Bearer $\{CRON_SECRET\}and return 401 otherwise. - Use the
dbsingleton from @/lib/supabase/database for DB ops. - Add the schedule to vercel.json's
cronsarray. Warn me if I'm on the Hobby plan limit (max 1 cron/day). - Tell me how to test it with curl + CRON_SECRET locally.
Follow CLAUDE.md conventions.
Testing locally
Manually hit your endpoint with the secret:
curl -H "Authorization: Bearer $CRON_SECRET" \
http://localhost:3000/api/cron/competitionsIt should return { "ok": true } (or whatever your handler returns).
Monitoring
Vercel Settings → Cron Jobs shows each cron's last run status and logs. Failures appear in your project's Logs tab with a cron- prefix.