├── .env.example ├── .github └── workflows │ ├── scheduled.yaml │ └── starter.yaml ├── README.md └── pages └── api ├── example.js └── example.ts /.env.example: -------------------------------------------------------------------------------- 1 | APP_KEY= 2 | -------------------------------------------------------------------------------- /.github/workflows/scheduled.yaml: -------------------------------------------------------------------------------- 1 | name: hourly-cron-job 2 | on: 3 | schedule: 4 | - cron: '*/60 * * * *' 5 | jobs: 6 | cron: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: hourly-cron-job 10 | run: | 11 | curl --request POST \ 12 | --url 'https://example.com/api/example' \ 13 | --header 'Authorization: Bearer ${{ secrets.ACTION_KEY }}' 14 | -------------------------------------------------------------------------------- /.github/workflows/starter.yaml: -------------------------------------------------------------------------------- 1 | name: Cron job 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | branches: [ main ] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # This workflow contains a single job called "build" 17 | build: 18 | # The type of runner that the job will run on 19 | runs-on: ubuntu-latest 20 | 21 | # Steps represent a sequence of tasks that will be executed as part of the job 22 | steps: 23 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 24 | - uses: actions/checkout@v2 25 | 26 | # Runs a single command using the runners shell 27 | - name: Run a one-line script 28 | run: echo Hello, world! 29 | 30 | # Runs a set of commands using the runners shell 31 | - name: Run a multi-line script 32 | run: | 33 | echo Add other actions to build, 34 | echo test, and deploy your project. 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Note** 2 | 3 | As of [February 22nd 2023](https://vercel.com/blog/cron-jobs) Vercel is officially offering built-in cron jobs to trigger your serverless and edge functions. Read the [documentation](https://vercel.com/docs/cron-jobs) to learn more. Keep in mind the feature is only free during its beta phase, it'll be a paid feature for general availability, which means that the GitHub Actions route will remain relevant for a completly free option. 4 | 5 | # Next.js Cron 6 | Cron jobs with [Github Actions](https://github.com/features/actions) for Next.js applications on Vercel▲ 7 | 8 | ## Motivation 9 | Since the Vercel platform is event-driven, therefore not maintaining a running server, you can't really schedule calls on your [API routes](https://nextjs.org/docs/api-routes/introduction) or [Serverless functions](https://vercel.com/docs/serverless-functions/introduction) in your Next.js application. 10 | Although there are many pre-existing services that provide scheduled cron jobs, I ultimately decided that [Github Actions](https://github.com/features/actions) suits my needs the best, since it integrates nicely with any project that already lives on Github, plus it's completely free. 11 | 12 | ## Get started 13 | All Github Actions reside in the directory `.github/workflows/` of your repository and are written in [YAML](https://yaml.org/). 14 | 15 | [`.github/workflows/starter.yaml`](https://github.com/baulml/nextjs-cron/blob/master/.github/workflows/starter.yaml) is the most basic workflow to help you get started with Actions. 16 | 17 | ## Scheduled tasks 18 | With [Scheduled events](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events) you can execute tasks at specified intervals. For instance, the provided workflow `.github/workflows/scheduled.yaml` executes a HTTP request with curl every 60 minutes. 19 | 20 | ```yaml 21 | name: Hourly cron job 22 | on: 23 | schedule: 24 | - cron: '*/60 * * * *' 25 | jobs: 26 | cron: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Hourly cron job 30 | run: | 31 | curl --request POST \ 32 | --url 'https://example.com/api/task' \ 33 | --header 'Authorization: Bearer ${{ secrets.ACTION_KEY }}' 34 | ``` 35 | If you are having trouble writing cron schedule expressions, take a look at [crontab guru](https://crontab.guru/). 36 | 37 | ## Next.js API routes 38 | 39 | [API routes](https://nextjs.org/docs/api-routes/introduction) and [Serverless functions](https://vercel.com/docs/serverless-functions/introduction) provide a straightforward solution to building your API with Next.js on Vercel. 40 | Any file inside the folder `pages/api` is mapped to `/api/*` and will be treated as an API endpoint instead of a `page`. 41 | 42 | If you are using serverless functions, regardless of the [Runtime](https://vercel.com/docs/runtimes), you would need to put the files into the `/api/` directory at your project's root. 43 | 44 | ### Authorization flow 45 | To securely trigger API routes and serverless functions with Github Actions, you need to provide an authorization key in the header of your API call, which, when executed, gets compared to a corresponding key in your Next.js application. 46 | 47 | You can achieve this by adding [Encrypted Secrets](https://docs.github.com/en/actions/reference/encrypted-secrets) to your Github repository and passing them with the header of your HTTP request, like shown in the previous code snippet. 48 | Along with adding the key to your Github repository, you also need to access it within your Next.js application, preferably through [Environment Variables](https://nextjs.org/docs/basic-features/environment-variables). 49 | 50 | The example `pages/api/example.js` implements this authorization flow. 51 | 52 | ```js 53 | export default function handler(req, res) { 54 | 55 | const { APP_KEY } = process.env; 56 | const { ACTION_KEY } = req.headers.authorization.split(" ")[1]; 57 | 58 | try { 59 | if (ACTION_KEY === APP_KEY) { 60 | // Process the POST request 61 | res.status(200).json({ success: 'true' }) 62 | } else { 63 | res.status(401) 64 | } 65 | } catch(err) { 66 | res.status(500) 67 | } 68 | } 69 | ``` 70 | 71 | Use `pages/api/example.ts` for Typescript. 72 | 73 | ```ts 74 | import type { NextApiRequest, NextApiResponse } from 'next' 75 | 76 | export default function handler(req:NextApiRequest, res:NextApiResponse) { 77 | 78 | const { APP_KEY } = process.env; 79 | const { ACTION_KEY } = req.headers.authorization.split(" ")[1]; 80 | 81 | try { 82 | if (ACTION_KEY === APP_KEY) { 83 | // Process the POST request 84 | res.status(200).json({ success: 'true' }) 85 | } else { 86 | res.status(401) 87 | } 88 | } catch(err) { 89 | res.status(500) 90 | } 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /pages/api/example.js: -------------------------------------------------------------------------------- 1 | export default function handler(req, res) { 2 | 3 | const { APP_KEY } = process.env; 4 | const { ACTION_KEY } = req.headers.authorization.split(" ")[1]; 5 | 6 | try { 7 | if (ACTION_KEY === APP_KEY) { 8 | // Process the POST request 9 | res.status(200).json({ success: 'true' }) 10 | } else { 11 | res.status(401) 12 | } 13 | } catch(err) { 14 | res.status(500) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pages/api/example.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | 3 | export default function handler(req:NextApiRequest, res:NextApiResponse) { 4 | 5 | const { APP_KEY } = process.env; 6 | const { ACTION_KEY } = req.headers.authorization.split(" ")[1]; 7 | 8 | try { 9 | if (ACTION_KEY === APP_KEY) { 10 | // Process the POST request 11 | res.status(200).json({ success: 'true' }) 12 | } else { 13 | res.status(401) 14 | } 15 | } catch(err) { 16 | res.status(500) 17 | } 18 | } 19 | --------------------------------------------------------------------------------