9.4 KiB
Cron Setup (PM2 / self-hosted Linux)
Aplikasi punya beberapa endpoint cron yang harus di-trigger periodik dari luar. Karena deploy pakai PM2 di VPS Linux (bukan Vercel), trigger schedule pakai system crontab — zero dependency, OS-native.
Audit trail otomatis: semua cron yang di-wrap
runCron()helper auto-log ke tabelCronRun(start/finish/error). Cek hasilnya real-time di/admin/system— link "System" di sidebar admin. Tidak perlu tail log untuk monitoring rutin.
Daftar cron job
| # | Endpoint | Schedule | Frekuensi | Tujuan |
|---|---|---|---|---|
| 1 | GET /api/cron/auto-complete-trips |
0 18 * * * |
Daily 01:00 WIB (18:00 UTC) | Flip trip yang sudah lewat tanggal selesai dari OPEN/FULL ke COMPLETED. Setelah itu, release payout HELD yang sudah lewat heldUntil. |
| 2 | GET /api/cron/process-email-jobs |
*/5 * * * * |
Setiap 5 menit | Drain retry queue email — pick EmailJob status PENDING/FAILED (attempts<5), retry via Resend dengan exponential backoff. |
| 3 | GET /api/cron/cleanup-trip-images |
30 18 * * * |
Daily 01:30 WIB (18:30 UTC) | Hapus file gambar trip yatim — foto yang di-upload di form create-trip tapi trip-nya batal dibuat. Hanya file >24 jam yang tak direferensikan TripImage. |
Semua cron pakai pola yang sama: header Authorization: Bearer ${CRON_SECRET}, idempotent, auto-log ke CronRun. Tambah cron baru = tambah baris di tabel ini + tabel TRACKED_JOBS di app/admin/system/page.tsx.
Setup di server (one-time)
1. Set CRON_SECRET di env production
Generate random secret 32 byte:
openssl rand -hex 32
Tambah ke file .env yang dibaca PM2 (atau yang pasti ter-load saat process boot):
CRON_SECRET="<hasil-openssl-tadi>"
Restart PM2 supaya proses re-load env:
pm2 restart setrip --update-env
2. Set env opsional untuk fitur lain yang di-trigger cron
| Env | Dibutuhkan oleh | Akibat kalau kosong |
|---|---|---|
RESEND_API_KEY |
process-email-jobs cron |
Email tetap di-queue di DB; cron skip dengan warning. Set nanti dan cron akan auto-drain queue. |
EMAIL_FROM |
process-email-jobs cron |
Pakai default SeTrip <onboarding@resend.dev> (cocok untuk dev/test). |
ADMIN_ALERT_WEBHOOK_URL |
runCron (semua cron) |
Tidak ada Discord push notif saat cron FAILED. Admin tetap bisa cek manual di /admin/system. |
Semua env ini ada di .env.example dengan instruksi setup masing-masing.
3. Daftarkan crontab
Edit crontab user yang punya akses ke env (biasanya user yang menjalankan PM2):
crontab -e
Tambah baris berikut (ganti https://your-domain.com dan <CRON_SECRET> sesuai env yang di-set di step 1):
# === Setrip cron jobs ===
# 1. Auto-complete trip + release payout (daily 01:00 WIB)
0 18 * * * curl -fsS -H "Authorization: Bearer <CRON_SECRET>" https://your-domain.com/api/cron/auto-complete-trips >> /var/log/setrip-cron.log 2>&1
# 2. Drain email retry queue (setiap 5 menit)
*/5 * * * * curl -fsS -H "Authorization: Bearer <CRON_SECRET>" https://your-domain.com/api/cron/process-email-jobs >> /var/log/setrip-cron.log 2>&1
# 3. Bersihkan gambar trip yatim (daily 01:30 WIB)
30 18 * * * curl -fsS -H "Authorization: Bearer <CRON_SECRET>" https://your-domain.com/api/cron/cleanup-trip-images >> /var/log/setrip-cron.log 2>&1
Verifikasi crontab tersimpan:
crontab -l
4. Siapkan file log
sudo touch /var/log/setrip-cron.log
sudo chown $(whoami) /var/log/setrip-cron.log
Optional — logrotate supaya log tidak menggemuk:
sudo nano /etc/logrotate.d/setrip-cron
Isi:
/var/log/setrip-cron.log {
weekly
rotate 4
compress
missingok
notifempty
}
Test manual (sanity check setelah deploy)
Sebelum tunggu schedule, panggil endpoint langsung untuk verifikasi:
# Test cron 1
curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://your-domain.com/api/cron/auto-complete-trips
# Test cron 2
curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://your-domain.com/api/cron/process-email-jobs
Expected response per cron:
| Cron | Sukses kosong | Sukses ada pekerjaan |
|---|---|---|
auto-complete-trips |
{"ok":true,"completed":0,"ids":[],"payoutsReleased":[]} |
{"ok":true,"completed":2,"ids":["clx...","cly..."],"payoutsReleased":["..."]} |
process-email-jobs |
{"ok":true,"picked":0,"succeeded":0,"failed":0} |
{"ok":true,"picked":5,"succeeded":5,"failed":0} |
cleanup-trip-images |
{"ok":true,"scanned":0,"deleted":0} |
{"ok":true,"scanned":12,"deleted":3} |
Error response:
- 401 —
CRON_SECRETdi env tidak match dengan header. Cekpm2 env <id>. - 500 "Server misconfigured" —
CRON_SECRETbelum di-set di env. - 500 lain — cek log app atau
/admin/systemuntuk detail error.
Monitoring
Cara utama: /admin/system (admin panel)
Buka /admin/system — tampilkan:
- Per-job summary card: last run, last success, count 7 hari, error count 7 hari, health badge (🟢 OK / 🟡 STALE / 🔴 FAILED).
- Recent runs table: 20 cron run terakhir lintas semua job (waktu, status, payload, error).
- Stale state alerts: banner kuning kalau ada Payment AWAITING > 25h / Payout HELD overdue / Refund APPROVED > 7d.
Cek setiap pagi sebelum mulai kerja — kalau semua 🟢 OK, tidak ada incident.
Cara backup: tail log file
tail -f /var/log/setrip-cron.log
PM2 log untuk console.log dari endpoint:
pm2 logs setrip --lines 100 | grep cron
Discord push notif (opsional)
Kalau env ADMIN_ALERT_WEBHOOK_URL di-set ke Discord webhook URL, runCron otomatis kirim 🚨 message saat cron FAILED — admin bisa react langsung tanpa harus cek dashboard.
Cara setup: Discord channel → Edit Channel → Integrations → Webhooks → New → copy URL → set di env.
Troubleshooting
Cron jalan tapi tidak ada efek di DB:
- Cek
/admin/system— kalau status SUCCESS denganpayload: { completed: 0 }, memang tidak ada pekerjaan saat ini. - Cek
pm2 logs setripuntuk error runtime. - Verifikasi waktu server:
date -u(output harus UTC kalau pakai schedule UTC).
Cron tidak jalan sama sekali:
- Cek service cron aktif:
systemctl status cron(Debian/Ubuntu) atausystemctl status crond(RHEL/CentOS). - Cek crontab terdaftar di user yang benar:
sudo crontab -u $(whoami) -l. - Cek
/admin/system— kalau "Last run" jauh dari ekspektasi (mis. > 25 jam untuk daily cron), schedule mungkin tidak ke-trigger.
process-email-jobs SUCCESS tapi email tidak terkirim:
- Cek
RESEND_API_KEYdi env. Tanpa env, cron return early dengan warning di log. - Cek dashboard Resend untuk delivery status / bounce.
- Cek tabel
EmailJobdi DB: statusPENDING/FAILED+lastErrorfield.
auto-complete-trips SUCCESS tapi trip masih OPEN:
- Cek
Trip.endDate(kalau ada) atauTrip.date— harus lewat cutoff (utcStartOfDay(now)). - Trip dengan status
CLOSEDsengaja tidak di-touch (organizer eksplisit batalkan).
Secret bocor:
- Generate ulang
CRON_SECRET, update di.env+ semua baris crontab, restart PM2.
Cron FAILED berturut-turut:
/admin/systemakan tampilkan badge 🔴 FAILED.- Kalau env
ADMIN_ALERT_WEBHOOK_URLdi-set, Discord channel akan dapat notif. - Klik "Last error" di card cron untuk lihat stack trace, atau cek tabel
CronRun.errorMessagelangsung.
Saat menambah cron baru (developer note)
Checklist:
- Buat route handler di
app/api/cron/<name>/route.tsdengan pola standar (CRON_SECRET check +runCron(jobName, fn)wrapper). - Tambah entry di tabel Daftar cron job di doc ini.
- Tambah baris di
TRACKED_JOBSdi app/admin/system/page.tsx supaya muncul di health card. - Brief ops: tambah baris di server crontab dengan schedule yang sesuai.
Pattern minimal cron handler:
// app/api/cron/<name>/route.ts
import { NextRequest, NextResponse } from "next/server";
import { runCron } from "@/lib/cron-runner";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function GET(req: NextRequest) {
const secret = process.env.CRON_SECRET;
if (!secret) {
return NextResponse.json({ error: "Server misconfigured" }, { status: 500 });
}
if (req.headers.get("authorization") !== `Bearer ${secret}`) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const outcome = await runCron("<jobName>", async () => {
// ... actual work; return value masuk ke CronRun.payload
return { processedCount: 0 };
});
if (!outcome.ok) {
return NextResponse.json({ error: outcome.error }, { status: 500 });
}
return NextResponse.json({ ok: true, ...outcome.payload });
}
Kalau pindah ke Vercel / PaaS lain
Bikin vercel.json di root:
{
"crons": [
{
"path": "/api/cron/auto-complete-trips",
"schedule": "0 18 * * *"
},
{
"path": "/api/cron/process-email-jobs",
"schedule": "*/5 * * * *"
}
]
}
Vercel Cron otomatis kirim header Authorization: Bearer <VERCEL_CRON_SECRET> — sesuaikan logic auth check di route handler (atau pakai env yang sama). Endpoint sudah platform-agnostic — tidak ada code change yang diperlukan.
Catatan: Vercel Cron free tier limit 2 cron/project + minimum schedule 1 jam. Untuk
process-email-jobsyang 5 menit, perlu upgrade Vercel Pro atau pertahankan VPS untuk cron.