Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/1.docs/50.tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ export default defineNitroConfig({
### Platform support

- `dev`, `node-server`, `bun` and `deno-server` presets are supported with [croner](https://croner.56k.guru/) engine.
- `cloudflare_module` preset have native integration with [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/). Make sure to configure wrangler to use exactly same patterns you define in `scheduledTasks` to be matched.
- `cloudflare_module` preset has native integration with [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/). Make sure to configure wrangler to use the same patterns you define in `scheduledTasks` to be matched.
- `vercel` preset has native integration with [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs). Nitro automatically generates the cron job configuration at build time β€” no manual `vercel.json` setup required.
- More presets (with native primitives support) are planned to be supported!

## Programmatically run tasks
Expand Down
28 changes: 28 additions & 0 deletions docs/2.deploy/20.providers/vercel.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,34 @@ When the proxy rule uses any of the following `ProxyOptions`, Nitro keeps it as
Response headers defined on the route rule via the `headers` option are still applied to CDN-level rewrites. Only request-level `ProxyOptions.headers` (sent to the upstream) require a runtime proxy.
::

## Scheduled tasks (Cron Jobs)

:read-more{title="Vercel Cron Jobs" to="https://vercel.com/docs/cron-jobs"}

Nitro automatically converts your [`scheduledTasks`](/docs/tasks#scheduled-tasks) configuration into [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs) at build time. Define your schedules in your Nitro config and deploy - no manual `vercel.json` cron configuration required.

```ts [nitro.config.ts]
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
experimental: {
tasks: true
},
scheduledTasks: {
// Run `cms:update` every hour
'0 * * * *': ['cms:update'],
// Run `db:cleanup` every day at midnight
'0 0 * * *': ['db:cleanup']
}
})
```

### Secure cron job endpoints

:read-more{title="Securing cron jobs" to="https://vercel.com/docs/cron-jobs/manage-cron-jobs#securing-cron-jobs"}

To prevent unauthorized access to the cron handler, set a `CRON_SECRET` environment variable in your Vercel project settings. When `CRON_SECRET` is set, Nitro validates the `Authorization` header on every cron invocation.

## Custom build output configuration

You can provide additional [build output configuration](https://vercel.com/docs/build-output-api/v3) using `vercel.config` key inside `nitro.config`. It will be merged with built-in auto-generated config.
Expand Down
15 changes: 15 additions & 0 deletions src/presets/vercel/preset.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { defineNitroPreset } from "../_utils/preset.ts";
import type { Nitro } from "nitro/types";
import { presetsDir } from "nitro/meta";
import { join } from "pathe";
import {
deprecateSWR,
generateFunctionFiles,
Expand All @@ -19,6 +21,7 @@ const vercel = defineNitroPreset(
},
vercel: {
skewProtection: !!process.env.VERCEL_SKEW_PROTECTION_ENABLED,
cronHandlerRoute: "/_vercel/cron",
},
output: {
dir: "{{ rootDir }}/.vercel/output",
Expand Down Expand Up @@ -50,6 +53,18 @@ const vercel = defineNitroPreset(
}
logger.info(`Using \`${serverFormat}\` entry format.`);
nitro.options.entry = nitro.options.entry.replace("{format}", serverFormat);

// Cron tasks handler
if (
nitro.options.experimental.tasks &&
Object.keys(nitro.options.scheduledTasks || {}).length > 0
) {
nitro.options.handlers.push({
route: nitro.options.vercel!.cronHandlerRoute || "/_vercel/cron",
lazy: true,
handler: join(presetsDir, "vercel/runtime/cron-handler"),
});
}
},
"rollup:before": (nitro: Nitro) => {
deprecateSWR(nitro);
Expand Down
31 changes: 31 additions & 0 deletions src/presets/vercel/runtime/cron-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { timingSafeEqual } from "node:crypto";
import { defineHandler, HTTPError } from "nitro/h3";
import { runCronTasks } from "#nitro/runtime/task";

export default defineHandler(async (event) => {
// Validate CRON_SECRET if set - https://vercel.com/docs/cron-jobs/manage-cron-jobs#securing-cron-jobs
const cronSecret = process.env.CRON_SECRET;
if (cronSecret) {
const authHeader = event.req.headers.get("authorization") || "";
const expected = `Bearer ${cronSecret}`;
const a = Buffer.from(authHeader);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
throw new HTTPError("Unauthorized", { status: 401 });
}
}

const cron = event.req.headers.get("x-vercel-cron-schedule");
if (!cron) {
throw new HTTPError("Missing x-vercel-cron-schedule header", { status: 400 });
}
Comment thread
RihanArfan marked this conversation as resolved.

await runCronTasks(cron, {
context: {},
payload: {
scheduledTime: Date.now(),
},
});

return { success: true };
});
12 changes: 12 additions & 0 deletions src/presets/vercel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,18 @@ export interface VercelOptions {
* Possible values are: `web` (default) and `node`.
*/
entryFormat?: "web" | "node";

/**
* The route path for the Vercel cron handler endpoint.
*
* When `experimental.tasks` and `scheduledTasks` are configured,
* Nitro registers a cron handler at this path that Vercel invokes
* on each scheduled cron trigger.
*
* @default "/_vercel/cron"
* @see https://vercel.com/docs/cron-jobs
*/
cronHandlerRoute?: string;
}

/**
Expand Down
13 changes: 13 additions & 0 deletions src/presets/vercel/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,19 @@ function generateBuildConfig(nitro: Nitro, o11Routes?: ObservabilityRoute[]) {
],
} as VercelBuildConfigV3);

// Cron jobs from scheduledTasks
if (
nitro.options.experimental.tasks &&
Object.keys(nitro.options.scheduledTasks || {}).length > 0
) {
const cronPath = nitro.options.vercel!.cronHandlerRoute || "/_vercel/cron";
const cronEntries = Object.keys(nitro.options.scheduledTasks).map((schedule) => ({
path: cronPath,
schedule,
}));
config.crons = [...cronEntries, ...(config.crons || [])];
}

// Early return if we are building a static site
if (nitro.options.static) {
return config;
Expand Down
11 changes: 11 additions & 0 deletions test/presets/vercel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ describe("nitro:preset:vercel:web", async () => {
.then((r) => JSON.parse(r));
expect(config).toMatchInlineSnapshot(`
{
"crons": [
{
"path": "/_vercel/cron",
"schedule": "* * * * *",
},
],
"overrides": {
"_scalar/index.html": {
"path": "_scalar",
Expand Down Expand Up @@ -317,6 +323,10 @@ describe("nitro:preset:vercel:web", async () => {
"dest": "/500",
"src": "/500",
},
{
"dest": "/_vercel/cron",
"src": "/_vercel/cron",
},
{
"dest": "/_swagger",
"src": "/_swagger",
Expand Down Expand Up @@ -403,6 +413,7 @@ describe("nitro:preset:vercel:web", async () => {
"functions/_openapi.json.func (symlink)",
"functions/_scalar.func (symlink)",
"functions/_swagger.func (symlink)",
"functions/_vercel",
"functions/api/cached.func (symlink)",
"functions/api/db.func (symlink)",
"functions/api/echo.func (symlink)",
Expand Down
Loading