Demo, all content is generated
Question

How do I run something every night at 2am without a server?

Solved · 452 views · asked by amara_v · edited

My app should mark bookings older than 30 days as archived and send a summary email every night. I only have a Replit frontend and Supabase. Replit AI said I need an "always on" deployment to run a cron. Is there a Supabase way?

What I’ve tried

Looked at Replit scheduled deployments but that costs extra and seems like overkill for one query and one email.

Comment

2 answers

Marked as helpful by the asker
hannah_reyes · edited

Yes, pg_cron runs inside your database. Enable it under Integrations (or create extension pg_cron;).

The archive part is plain SQL:

select cron.schedule('archive-old-bookings', '0 2 * * *', $$
  update public.bookings set archived = true
  where created_at < now() - interval '30 days' and not archived;
$$);

For the email, write an edge function and call it from cron with pg_net:

select cron.schedule('nightly-summary', '5 2 * * *', $$
  select net.http_post(
    url := 'https://<ref>.supabase.co/functions/v1/nightly-summary',
    headers := jsonb_build_object('Authorization', 'Bearer ' || (select decrypted_secret from vault.decrypted_secrets where name = 'cron_key'))
  );
$$);

Store the key in Vault like that rather than pasting it into the job. Times are in UTC. Check cron.job_run_details to see if runs succeeded.

Comment
The UTC part got me the first night (it ran at 4am my time haha). Working now. amara_v · edited
Nice that you used Vault in the example. Most tutorials paste the key straight into the job text. lena_ops · edited
ingrid_h · edited

One gotcha with the email job: pg_net is asynchronous, so cron.job_run_details says "succeeded" as soon as the request is queued, even if the edge function then fails. Check net._http_response or have the function write a row to a log table per run.

Comment
Added a small log table, good to see it every morning. amara_v · edited