npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

croncall

v0.1.1

Published

Cron jobs for Next.js. Serverless-native.

Readme

croncall

Cron jobs for Next.js. Serverless-native.

Zero runtime dependencies. TypeScript-first. Works with Vercel Cron out of the box.

Install

npm install croncall

Quick Start

1. Define your jobs

// lib/jobs.ts
import { createClockTower } from "croncall";

export const tower = createClockTower({
  jobs: {
    syncUsers: {
      schedule: "0 * * * *", // every hour
      handler: async () => {
        await db.syncUsersFromExternalAPI();
      },
      description: "Sync users from external API",
      retry: { maxAttempts: 3, backoff: "exponential" },
      timeout: 30_000,
    },
    sendDigest: {
      schedule: "0 9 * * 1", // Mondays at 9 AM UTC
      handler: async () => {
        await email.sendWeeklyDigest();
      },
      description: "Send weekly digest email",
    },
    cleanupSessions: {
      schedule: "@daily",
      handler: async () => {
        await db.deleteExpiredSessions();
      },
    },
  },
  secret: process.env.CRON_SECRET,
});

2. Create a route handler

// app/api/cron/route.ts
import { createCronHandler } from "croncall/next";
import { tower } from "@/lib/jobs";

export const GET = createCronHandler(tower);

3. Deploy

Add cron schedules to vercel.json:

{
  "crons": [
    { "path": "/api/cron?job=syncUsers", "schedule": "0 * * * *" },
    { "path": "/api/cron?job=sendDigest", "schedule": "0 9 * * 1" },
    { "path": "/api/cron?job=cleanupSessions", "schedule": "0 0 * * *" }
  ]
}

Or generate it programmatically:

import { generateVercelCron } from "croncall/next";
import { tower } from "./lib/jobs";

console.log(JSON.stringify(generateVercelCron(tower, "/api/cron"), null, 2));

Job Definition

Each job has:

| Field | Type | Required | Description | |---------------|--------------------------------|----------|--------------------------------------| | schedule | string | Yes | Cron expression or shortcut | | handler | () => Promise<void> | Yes | Async function to execute | | description | string | No | Human-readable description | | retry | { maxAttempts, backoff, baseDelay? } | No | Retry on failure | | timeout | number | No | Max execution time in ms |

Cron Syntax

Standard 5-field cron expressions:

 ┌───────────── minute (0-59)
 │ ┌───────────── hour (0-23)
 │ │ ┌───────────── day of month (1-31)
 │ │ │ ┌───────────── month (1-12)
 │ │ │ │ ┌───────────── day of week (0-6, Sun=0)
 │ │ │ │ │
 * * * * *

Supported features:

  • Wildcards: *
  • Ranges: 1-5
  • Lists: 1,3,5
  • Steps: */15, 1-30/2
  • Month names: jan, feb, ..., dec
  • Day names: sun, mon, ..., sat

Shortcuts:

| Shortcut | Equivalent | |--------------|-----------------| | @hourly | 0 * * * * | | @daily | 0 0 * * * | | @midnight | 0 0 * * * | | @weekly | 0 0 * * 0 | | @monthly | 0 0 1 * * | | @yearly | 0 0 1 1 * | | @annually | 0 0 1 1 * |

Vercel Cron Integration

Clocktower is designed to work with Vercel Cron Jobs.

Authentication

Vercel sends a CRON_SECRET environment variable and includes it in the Authorization: Bearer <secret> header. Clocktower validates this automatically:

  1. Checks options.secret passed to createCronHandler
  2. Falls back to config.secret from createClockTower
  3. Falls back to process.env.CRON_SECRET

If no secret is configured, requests are allowed without authentication.

Generating vercel.json

import { generateVercelCron } from "croncall/next";
import { tower } from "./lib/jobs";

const crons = generateVercelCron(tower, "/api/cron");
// [{ path: "/api/cron?job=syncUsers", schedule: "0 * * * *" }, ...]

Manual Triggers

Run a specific job on demand:

const result = await tower.run("syncUsers");
console.log(result);
// { success: true, duration: 1234 }

Run all due jobs:

const results = await tower.runDue();
for (const [name, result] of results) {
  console.log(`${name}: ${result.success ? "ok" : result.error}`);
}

Via HTTP (useful for testing):

# Run a specific job
curl http://localhost:3000/api/cron?job=syncUsers \
  -H "Authorization: Bearer your-secret"

# Run all due jobs
curl http://localhost:3000/api/cron \
  -H "Authorization: Bearer your-secret"

Inspect the Schedule

const schedule = tower.schedule();
// [
//   { jobName: "syncUsers", nextRun: 2026-03-26T15:00:00.000Z, schedule: "0 * * * *" },
//   { jobName: "sendDigest", nextRun: 2026-03-30T09:00:00.000Z, schedule: "0 9 * * 1" },
// ]

Retry & Error Handling

Configure retries per job:

{
  retry: {
    maxAttempts: 3,          // retry up to 3 times after initial failure
    backoff: "exponential",  // or "linear"
    baseDelay: 1000,         // 1s base delay (default)
  }
}
  • Exponential: delays of 1s, 2s, 4s, 8s, ...
  • Linear: delays of 1s, 2s, 3s, 4s, ...

The JobResult includes retryCount when retries were attempted:

const result = await tower.run("syncUsers");
if (!result.success) {
  console.error(`Failed after ${result.retryCount} retries: ${result.error}`);
}

TypeScript

All types are exported:

import type {
  CronExpression,
  JobDefinition,
  JobRegistry,
  ClockTowerConfig,
  ClockTower,
  JobResult,
  JobExecution,
  ScheduleEntry,
  RetryConfig,
} from "croncall";

Job names are fully typed:

const tower = createClockTower({
  jobs: {
    syncUsers: { schedule: "@hourly", handler: async () => {} },
  },
});

tower.run("syncUsers");    // OK
tower.run("nonexistent");  // Type error

License

MIT