Scheduled Jobs (Cron)

12 automated background jobs that power billing, notifications, and reconciliation.

Overview

DoorStax runs 12 scheduled background jobs that handle recurring billing, notification delivery, financial reconciliation, and data maintenance. All jobs are protected by the withCronGuard middleware, which verifies the CRON_SECRET header, prevents overlapping executions, and records each run in theCronRun model.

Job Reference

The following table lists all scheduled jobs, their cron schedules, and what they do.

JobScheduleTimingDescription
generate-charges0 6 1 * *1st of month, 6 AM UTCCreates monthly rent CHARGE ledger entries
generate-statements0 8 1 * *1st of month, 8 AM UTCGenerates owner statement PDFs
recurring-expenses0 7 1 * *1st of month, 7 AM UTCDuplicates recurring expenses and tenant invoicing
lease-expiration0 7 * * *Daily, 7 AM UTCTiered lease expiry alerts (90/60/30/14/7 days)
reconciliation0 9 * * *Daily, 9 AM UTCLedger balance verification and auto-fix
retry-webhooks30 9 * * *Daily, 9:30 AM UTCRetries failed webhook deliveries
generate-payouts0 10 2 * *2nd of month, 10 AM UTCCalculates owner payouts
financial-reconciliation0 11 * * *Daily, 11 AM UTCKadima to local payment matching
process-jobs0 12 * * *Daily, 12 PM UTCDrains queued background jobs
autopay-reminders0 14 * * *Daily, 2 PM UTCPre-charge and enrollment reminders
rent-reminders0 15 * * *Daily, 3 PM UTC3-day reminder for non-autopay tenants
overdue-notices0 16 * * *Daily, 4 PM UTCEscalating overdue notices (1/5/15/30 days)

Cron Guard

All cron endpoints are wrapped with the withCronGuard middleware that provides three layers of protection:

ProtectionDescription
Secret VerificationValidates the CRON_SECRET header matches the environment variable
Overlap PreventionChecks for an active CronRun with status RUNNING and skips if found
Run TrackingCreates a CronRun record with start time, updates with status and duration on completion
withCronGuard Usage
import { withCronGuard } from "@/lib/cron-guard";

export const POST = withCronGuard("generate-charges", async () => {
  // Find all active leases
  const leases = await prisma.lease.findMany({
    where: { status: "ACTIVE" },
    include: { unit: true, property: true },
  });

  let created = 0;
  for (const lease of leases) {
    await prisma.ledgerEntry.create({
      data: {
        type: "CHARGE",
        amount: lease.rentAmount,
        leaseId: lease.id,
        description: `Rent - ${format(new Date(), "MMMM yyyy")}`,
      },
    });
    created++;
  }

  return { chargesCreated: created };
});

Monitoring

Every cron execution is tracked in the CronRun model. This provides a full audit log of when each job ran, how long it took, whether it succeeded or failed, and a summary JSON with job-specific metrics.

CronRun Record
{
  "id": "crun_001",
  "jobName": "generate-charges",
  "status": "COMPLETED",
  "startedAt": "2025-02-01T06:00:00.000Z",
  "completedAt": "2025-02-01T06:00:12.340Z",
  "durationMs": 12340,
  "summary": {
    "chargesCreated": 847,
    "leasesProcessed": 847,
    "errors": 0
  }
}

Best Practice: Monitor CronRun records for jobs with status FAILED or unusually long durations. Set up alerts for any job that has not run within its expected schedule window.