Skip to main content
Localizia

Background jobs

Overview

The kit runs as a single web process (no worker, no queue). Most maintenance is deliberately cron-free — trial reminders are Stripe webhooks, credit refills accrue lazily on read, bans and sessions expire on the next check. What remains is occasional cleanup, and that's what the jobs scaffold in src/lib/jobs/ handles: an external scheduler hits an authenticated endpoint on a schedule and the app runs the registered jobs.

This design needs no extra infrastructure, doesn't double-run across instances, and survives deploys (there's no in-process timer to lose).

Enabling it

Set a secret. Until you do, the endpoint returns 404 (feature off):

CRON_SECRET=<a long random string, 32+ chars>

Then point a scheduler at /api/cron/run with the secret as a Bearer token. A VPS crontab, every 15 minutes:

*/15 * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" \
  https://your-app.com/api/cron/run

Or a GitHub Actions schedule:

on:
  schedule:
    - cron: "*/15 * * * *"
jobs:
  cron:
    runs-on: ubuntu-latest
    steps:
      - run: |
          curl -fsS -H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
            https://your-app.com/api/cron/run

The endpoint

GET (or POST) /api/cron/run:

  • No CRON_SECRET set404.
  • Missing/wrong Bearer token401 (compared in constant time).
  • Valid → runs every job and returns { ok, ran: [{ name, ok, result, durationMs }] }.

A job that throws is isolated: it's logged and reported as ok: false, but the other jobs still run. The response is 500 if any job failed, 200 otherwise.

Adding a job

Jobs live in src/lib/jobs/. Write a Job and register it — nothing else to wire:

// src/lib/jobs/my-job.ts
import type { Job } from "./types";

export const myJob: Job = {
  name: "my-job",
  async run({ prisma, logger }) {
    const { count } = await prisma.something.deleteMany({ where: { … } });
    return { cleaned: count };
  },
};
// src/lib/jobs/index.ts
export const jobs: Job[] = [purgeExpiredInvitations, myJob];

Jobs must be idempotent and safe to run concurrently — schedulers can overlap or retry. Keep each run bounded (batch/limit large deletes).

The shipped jobs

All are idempotent cleanup sweeps — safe to keep, remove, or copy:

JobWhat it does
purge-expired-invitationsRemoves pending invitations past their expiry
purge-expired-sessionsRemoves sessions past expiresAt (already invalid)
purge-expired-verificationsRemoves expired verification/reset tokens
purge-old-login-eventsTrims sign-in history older than 90 days
purge-old-notificationsRemoves read notifications older than 90 days (unread kept)
release-stale-credit-reservationsReleases credit holds stuck in held > 24h, reclaiming the credits
purge-soft-deleted-usersHard-deletes users an admin soft-deleted > 30 days ago (their grace/restore window)

Most are deleteMany calls; release-stale-credit-reservations reuses the billing releaseReservation primitive to refund abandoned holds.