DocumentationSearch docs
  1. 01Getting started
  2. 02Next.js and Vercel
  3. 03Node servers and scripts
  4. More JavaScript platforms

    1. 04SvelteKit
    2. 05Nuxt and Nitro
    3. 06React Router and Remix
    4. 07NestJS
    5. 08Strapi
    6. 09Netlify
    7. 10Firebase
    8. 11Convex
    9. 12Trigger.dev
    10. 13Inngest
    11. 14Cloudflare Workers
    12. 15Supabase and pg_cron
  5. Ruby

    1. 16Ruby on Rails
    2. 17Ruby
  6. Python

    1. 18Django
    2. 19Celery
    3. 20Python
  7. PHP

    1. 21Laravel
    2. 22Symfony
    3. 23WordPress
    4. 24Drupal
    5. 25Craft CMS
    6. 26PHP
  8. Go

    1. 27Go
    2. 28Go schedulers
  9. Rust

    1. 29Rust
    2. 30Rust schedulers
  10. Elixir

    1. 31Elixir
    2. 32Elixir schedulers
  11. Java

    1. 33Java
    2. 34Java schedulers
  12. .NET

    1. 35.NET
    2. 36.NET schedulers
  13. Reference

    1. 37Schedules, grace, and timeouts
    2. 38What it catches
    3. 39Alerts
    4. 40AI triage
    5. 41Stores
    6. 42Dashboard and API
    7. 43MCP server
    8. 44Agent skill
    9. 45TypeScript API reference
    10. 46Limits and design notes
    11. 47Environment variables
    12. 48Stability
    13. 49Deprecations

SvelteKit

SvelteKit

SvelteKit has no scheduler of its own. Something outside calls an endpoint on a schedule: Vercel cron, a Netlify scheduled function, GitHub Actions, or crontab running curl. Each job is a +server.ts endpoint wrapped with handler(), and the dashboard is one catch-all endpoint.

The client

Keep it under src/lib/server so SvelteKit never lets it reach the browser.

// src/lib/server/cronwatch.ts
import { cronwatch } from "@cronwatch/sdk";
import { postgres } from "@cronwatch/sdk/postgres";
import { slack } from "@cronwatch/sdk/slack";

export const cw = cronwatch({
  store: postgres({ connectionString: process.env.DATABASE_URL }),
  alerts: [slack({ webhookUrl: process.env.SLACK_WEBHOOK_URL! })],
  defaults: { timezone: "UTC" },
});

export const digest = cw.job("daily-digest", { schedule: "0 6 * * *", grace: "10m", timeout: "5m" });

Which store depends on the adapter. On adapter-vercel or adapter-netlify there is no disk, so use Postgres. On adapter-node with a persistent volume, SQLite works (sqlite({ path: "./data/cronwatch.db" }), from @cronwatch/sdk/sqlite). The stores are Node drivers, so the server code must run on Node.

The SDK reads CRON_SECRET, CRONWATCH_TOKEN, and DATABASE_URL from process.env, which every Node adapter populates at runtime. To pass values from $env/dynamic/private instead, give them to cronwatch({ cronSecret }), cw.routes({ token }), and postgres({ connectionString }).

A job endpoint

// src/routes/api/cron/daily-digest/+server.ts
import { digest } from "$lib/server/cronwatch";
import type { RequestHandler } from "./$types";

const run = digest.handler(async (job) => {
  const sent = await sendDigest();
  job.metric("emails", sent);
});

export const GET: RequestHandler = ({ request }) => run(request);

handler() checks Authorization: Bearer <CRON_SECRET>, runs the function, records the run, and answers 200 or 500. Point the platform’s cron at the path. On Vercel that is vercel.json, whose crons run in UTC, the reason for timezone: "UTC" above:

{
  "crons": [
    { "path": "/api/cron/daily-digest", "schedule": "0 6 * * *" },
    { "path": "/cronwatch/api/check",   "schedule": "*/10 * * * *" }
  ]
}

The dashboard

A rest parameter matches /cronwatch itself as well as everything under it, so one file serves the whole dashboard and API.

// src/routes/cronwatch/[...path]/+server.ts
import { cw } from "$lib/server/cronwatch";
import type { RequestHandler } from "./$types";

const routes = cw.routes({ basePath: "/cronwatch" });

export const GET: RequestHandler = ({ request }) => routes.handler(request);
export const POST = GET;
export const DELETE = GET;

Set CRONWATCH_TOKEN and open /cronwatch?token=<it> once. SvelteKit redirects /cronwatch/ to /cronwatch, which is harmless here.

The dashboard’s forms post back to the same origin and CronWatch refuses cross-site posts, so the request URL SvelteKit builds must have the public origin. On adapter-node behind a proxy, set ORIGIN (or PROTOCOL_HEADER and HOST_HEADER) as the adapter’s docs describe; SvelteKit’s own CSRF check needs the same. Setting the adapter’s ORIGIN is the fix for both; the routes' origin option (behind a proxy) would only cover CronWatch’s check.

Where the check runs

On adapter-node the server stays up, so it can run the check itself. The init hook runs once before the first request:

// src/hooks.server.ts
import type { ServerInit } from "@sveltejs/kit";
import { cw } from "$lib/server/cronwatch";

export const init: ServerInit = () => {
  cw.startChecking("1m");
};

On serverless adapters, leave that out and let the platform cron hit /cronwatch/api/check with CRON_SECRET as the bearer, as in the vercel.json above. The check endpoint accepts the cron secret, so the cron never needs the dashboard token.

Limits that matter

A function killed by the platform’s time limit cannot record its own end. The run stays running until a check passes the job’s timeout, then it is marked stuck. Set timeout a little above the function’s maximum duration.