Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions src/build/rolldown/prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export async function buildProduction(nitro: Nitro, config: RolldownOptions) {
const buildStartTime = Date.now();

await scanHandlers(nitro);
nitro.routing.sync();
await writeTypes(nitro);

let output: RolldownOutput | undefined;
Expand Down
1 change: 1 addition & 0 deletions src/build/rollup/prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export async function buildProduction(nitro: Nitro, rollupConfig: RollupConfig)
const buildStartTime = Date.now();

await scanHandlers(nitro);
nitro.routing.sync();
Comment thread
RihanArfan marked this conversation as resolved.
Outdated
await writeTypes(nitro);

let output: RollupOutput | undefined;
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,
cronHandlerPath: "/_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!.cronHandlerPath!,
Comment thread
RihanArfan marked this conversation as resolved.
Outdated
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.

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

return { cron, tasks: result };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this object consumed by vercel? Why this specific shape?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No particular reason. I've double checked and Vercel only shows things that were logged but not the response of this endpoint, so I'll update it so we return nothing. Should we log anything? Like Running cron schedule * * * * *

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We mighjt like success: true for example. My main concern is to not introduce a reponse and break it later only

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorted

});
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
*/
cronHandlerPath?: string;
Comment thread
RihanArfan marked this conversation as resolved.
Outdated
}

/**
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!.cronHandlerPath!;
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