How to Schedule a Supabase Edge Function with Cronhooks
Supabase Edge Functions are great for running serverless logic — but they only run when someone calls them. If you need them to run automatically on a schedule (every hour, every day, every Monday at 9am), you need an external scheduler.
That's where Cronhooks comes in. In this guide, you'll learn how to trigger a Supabase Edge Function on any recurring schedule using Cronhooks — no infrastructure to manage, no cron servers to maintain.
What we'll build
A Supabase Edge Function that runs on a daily schedule. The example we'll use is a daily database cleanup job — a real-world use case that almost every Supabase app eventually needs. The same approach works for:
- Sending scheduled digest emails
- Syncing data with a third-party API
- Generating daily/weekly reports
- Running health checks on your services
- Expiring stale records or sessions
Prerequisites
Before starting, make sure you have:
- A Supabase project (free tier works fine)
- The Supabase CLI installed
- A Cronhooks account (free tier works for testing)
- Node.js 18+ installed locally
Step 1: Create the Supabase Edge Function
First, initialise a Supabase project locally if you haven't already:
supabase init
Then create a new Edge Function:
supabase functions new daily-cleanup
This creates a file at supabase/functions/daily-cleanup/index.ts. Open it and replace the contents with this:
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
serve(async (req: Request) => {
// Optional but recommended: verify the request is from Cronhooks
const cronhooksSignature = req.headers.get("x-cronhooks-signature");
const expectedSecret = Deno.env.get("CRONHOOKS_SECRET");
if (expectedSecret && cronhooksSignature !== expectedSecret) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
// Create a Supabase client using the service role key
// (service role bypasses Row Level Security — safe to use server-side)
const supabase = createClient(
Deno.env.get("SUPABASE_URL") ?? "",
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""
);
// Example: delete records older than 30 days from a `logs` table
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const { error, count } = await supabase
.from("logs")
.delete({ count: "exact" })
.lt("created_at", thirtyDaysAgo.toISOString());
if (error) {
console.error("Cleanup failed:", error.message);
return new Response(JSON.stringify({ success: false, error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
console.log(`Cleanup complete. Deleted ${count} records.`);
return new Response(
JSON.stringify({ success: true, deleted: count }),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
);
});
What this function does
- Verifies the request came from Cronhooks using a shared secret (recommended)
- Connects to your Supabase database using the service role key
- Deletes log records older than 30 days
- Returns a JSON response with the result
Step 2: Set up environment variables
Your function needs three environment variables. Add them to your local .env file for testing:
SUPABASE_URL=https://your-project-ref.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
CRONHOOKS_SECRET=any-long-random-string-you-choose
You can find SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in your Supabase dashboard under Project Settings → API.
For CRONHOOKS_SECRET, just generate a random string — you'll paste this into Cronhooks as a custom header value later. This ensures only Cronhooks can trigger your function.
Step 3: Deploy the function to Supabase
Deploy your function with:
supabase functions deploy daily-cleanup --no-verify-jwt
Note: We use
--no-verify-jwtbecause Cronhooks will call this function as an external service, not as an authenticated Supabase user. We're handling our own auth via theCRONHOOKS_SECRETheader instead.
Now set the secrets in your Supabase project:
supabase secrets set CRONHOOKS_SECRET=your-long-random-string
SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are automatically available inside Edge Functions — you don't need to set those manually.
Step 4: Get your function's URL
Your Edge Function is now live at:
https://<your-project-ref>.supabase.co/functions/v1/daily-cleanup
You can find your project ref in the Supabase dashboard URL or under Project Settings → General.
Test it manually with curl to confirm it's working:
curl -X POST https://<your-project-ref>.supabase.co/functions/v1/daily-cleanup \
-H "Content-Type: application/json" \
-H "x-cronhooks-signature: your-long-random-string"
You should get back:
{ "success": true, "deleted": 0 }
Step 5: Create a recurring schedule in Cronhooks
Now the fun part. Log in to Cronhooks and create a new schedule.
Fill in the schedule details:
Webhook URL:
https://<your-project-ref>.supabase.co/functions/v1/daily-cleanup
Method: POST
Headers: Add a custom header to authenticate your request:
| Header name | Value |
|---|---|
x-cronhooks-signature |
will be calculated and sent by Cronhooks |
Content-Type |
application/json |
Schedule type: Recurring
Cron expression: 0 2 * * *
This runs every day at 2:00 AM UTC. Some useful cron expressions for common schedules:
| Schedule | Cron expression |
|---|---|
| Every day at midnight | 0 0 * * * |
| Every day at 2am | 0 2 * * * |
| Every Monday at 9am | 0 9 * * 1 |
| Every hour | 0 * * * * |
| Every 15 minutes | */15 * * * * |
| First day of every month | 0 0 1 * * |
Timezone: Select your preferred timezone — Cronhooks handles the conversion.
Hit Save, and you're done.
Step 6: Verify it's working
Test immediately
On the schedule detail page in Cronhooks, click Trigger now to fire the webhook immediately. Check the execution log to confirm a 200 OK response came back.
Check your Supabase logs
In the Supabase dashboard, go to Edge Functions → daily-cleanup → Logs. You should see an entry like:
Cleanup complete. Deleted 0 records.
Monitor ongoing executions
Cronhooks keeps a history of every webhook execution — including the response status code and body. If your function fails, Cronhooks will also send you an email or Slack alert (depending on your plan) so you're never in the dark.
What happens if the function fails?
Cronhooks detects failures automatically — any response that isn't a 2xx status code is treated as a failure. You'll get:
- An email alert sent to your account email
- A Slack notification (if configured)
- A failure log in your schedule's execution history
This makes it easy to debug issues without constantly checking logs manually.
Taking it further
Once you have the basic pattern working, you can apply it to anything:
Send a weekly summary email: Replace the cleanup logic with a call to Resend, Sendgrid, or Postmark. Schedule it every Monday morning.
Sync with an external API: Pull data from a third-party API and upsert it into your Supabase database on a schedule.
Rotate API keys or tokens: Automatically refresh expiring credentials before they cause downtime.
Run database maintenance: VACUUM your tables, rebuild indexes, or archive old records.
The pattern is always the same: write the logic in your Edge Function, deploy it, and point Cronhooks at it.
Summary
Here's what we did:
- Created a Supabase Edge Function with a shared-secret auth check
- Deployed it with
--no-verify-jwtto allow external calls - Got the function's public URL from the Supabase dashboard
- Created a recurring Cronhooks schedule pointing at that URL
- Added a custom header so only Cronhooks can trigger the function
Supabase Edge Functions + Cronhooks is a clean, serverless stack for any scheduled job. No cron servers. No always-on compute. No infrastructure headaches.