# Supabase keep-alive: a copy-paste workflow so free projects stop pausing

> Supabase pauses free projects after about 7 days without database activity. Restore it in the dashboard, then add this self-contained GitHub Actions workflow: one real database read a day, retries, and a clear error if the project is already paused.

Source: https://scribase.com/supabase-keep-alive

Supabase pauses Free Plan projects after about 7 days with no database activity. Restore the project in the Supabase dashboard, then add the workflow below to any GitHub repository. It reads one row a day through the REST API, so the project never looks idle.

## The workflow

Add two repository secrets under Settings, Secrets and variables, Actions: SUPABASE_URL (for example https://abcdefgh.supabase.co) and SUPABASE_ANON_KEY (Project Settings, API, anon public key). Save the file below as .github/workflows/keep-alive.yml, push it, and run it once from the Actions tab.

It uses no marketplace action, so there is nothing to update and nothing that can be taken down. Every line it runs is in the file.

- Reads one row from a table through the REST API with your anon key, the same call your app makes.
- Retries network errors, 429 and 5xx with 2s, 4s, 8s back-off.
- Treats HTTP 540 as "already paused": no retry, a clear error, and a restore reminder in the job summary.
- Writes a job summary (project, time, request, status) you can read without opening the logs.

_.github/workflows/keep-alive.yml_

```yaml
# .github/workflows/keep-alive.yml
# Keeps a free Supabase project from pausing: one real database read a day through the REST API.
# Repository secrets: SUPABASE_URL (https://<ref>.supabase.co) and SUPABASE_ANON_KEY (anon public key).
# No marketplace action, nothing to update: plain bash and curl.
name: Keep Supabase alive

on:
  schedule:
    - cron: "17 6 * * *"   # daily, 06:17 UTC; avoid :00, GitHub's busiest minute
  workflow_dispatch:        # lets you run it by hand from the Actions tab

permissions: {}

jobs:
  ping:
    runs-on: ubuntu-latest
    steps:
      - name: Read one row through the REST API
        env:
          SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
          SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
          TABLE: keepalive   # any small table; RLS with no anon policy is fine, the read returns []
        run: |
          set -u
          url="${SUPABASE_URL%/}"
          host="${url#*://}"
          path="/rest/v1/${TABLE}?select=*&limit=1"
          attempt=0
          delay=2
          while :; do
            attempt=$((attempt + 1))
            code=$(curl -sS -o body.txt -w '%{http_code}' --max-time 20 \
              -H "apikey: $SUPABASE_ANON_KEY" \
              -H "Authorization: Bearer $SUPABASE_ANON_KEY" \
              -H "Accept: application/json" \
              "$url$path") || code=000
            echo "GET $path -> HTTP $code (attempt $attempt)"
            case "$code" in
              540) break ;;                       # paused: retrying cannot help
              000|429|5[0-9][0-9])                # network error, rate limit, server error: retry
                if [ "$attempt" -le 3 ]; then
                  echo "  transient, retrying in ${delay}s"
                  sleep "$delay"
                  delay=$((delay * 2))
                  continue
                fi
                ;;
            esac
            break
          done
          case "$code" in
            2*)  title="ok" ;;
            540) title="project is PAUSED" ;;
            *)   title="failed" ;;
          esac
          {
            echo "## Supabase keep-alive: $title"
            echo
            echo "Project: \`$host\`  "
            echo "Time: $(date -u +'%Y-%m-%d %H:%M:%S UTC')  "
            echo "Request: \`GET $path\` -> HTTP $code after $attempt attempt(s)"
            if [ "$code" = "540" ]; then
              echo
              echo "Restore it in the Supabase dashboard: open the project and click **Restore project**. Free projects can be restored for one year after pausing."
            fi
          } >> "${GITHUB_STEP_SUMMARY:-/dev/null}"
          case "$code" in
            2*)
              echo "ok: $host answered, the project is awake"
              exit 0 ;;
            540)
              echo "::error::$host answered HTTP 540: the project is paused. Restore it in the Supabase dashboard (Restore project); a workflow cannot unpause it."
              exit 1 ;;
            401|403)
              echo "::error::anon key rejected by $host (HTTP $code). Check that SUPABASE_ANON_KEY belongs to this project."
              exit 1 ;;
            404)
              echo "::error::table '$TABLE' not found through the REST API (HTTP 404). Create it (see the SQL on the page) or change TABLE."
              exit 1 ;;
            000)
              echo "::error::could not reach $host (DNS, TLS or timeout after $attempt attempts). Check SUPABASE_URL."
              exit 1 ;;
            *)
              echo "::error::$host answered HTTP $code: $(tr -d '\n' < body.txt | cut -c1-200)"
              exit 1 ;;
          esac

```

## Pick a table to read

Any small existing table works; change TABLE in the workflow. If you would rather not point it at real data, run this once in the SQL editor:

- Row Level Security with no policy for anon is fine: the read runs and returns an empty list, so nothing is exposed.
- The anon key ships in your frontend by design, but keep it in a repository secret so it is masked in logs and not indexed.

_Supabase SQL editor_

```sql
create table if not exists keepalive (
  id bigint generated always as identity primary key,
  created_at timestamptz not null default now()
);
alter table keepalive enable row level security;  -- no policy: anon reads return [] but the query runs
grant select on keepalive to anon;
```

## Why Supabase pauses free projects

- The pricing page says it plainly: free projects are paused after 1 week of inactivity, and the Free plan allows 2 active projects.
- What counts as activity: opening the project in the dashboard or sending real requests from your app. Supabase does not publish the exact threshold.
- You get an email about a week before a pause, and another once it happens.
- Paid plans are not paused for inactivity.

## Sources

- [Supabase: Pricing](https://supabase.com/pricing)
- [Supabase: Going into production](https://supabase.com/docs/guides/platform/going-into-prod)
- [Supabase FAQ: Do projects pause when they are inactive?](https://supabase.com/kb/faqs/platform/free-project-pausing)

## How to tell it is paused, and how to get it back

1. **Check the status code** — A paused project answers every API request with HTTP 540. In a browser the symptom is usually a bare network error ("Failed to fetch"), because the paused gateway answers without CORS headers.

```sh
curl -s -o /dev/null -w '%{http_code}\n' \
  -H "apikey: $SUPABASE_ANON_KEY" "https://YOUR-REF.supabase.co/rest/v1/"
# 200 = awake, 540 = paused
```

2. **Click Restore project in the dashboard** — Open the project and click Restore project. It takes a few minutes. A paused project keeps all of its data and settings.

3. **Make sure you have a free slot** — The Free plan allows 2 active projects. Paused projects do not count, so if two others are running, pause one first.

4. **Do not wait too long** — You have one year to restore from the dashboard. After that, the backup file and Storage objects can still be downloaded from the project page until the project is deleted. The dashboard shows a countdown on paused projects.

5. **Add the workflow the same day** — A restored project is paused again if the next 7 days are quiet.

## Sources

- [Supabase: HTTP status codes](https://supabase.com/docs/guides/platform/http-status-codes)
- [Supabase FAQ: Do projects pause when they are inactive?](https://supabase.com/kb/faqs/platform/free-project-pausing)
- [Supabase: Upgrading and restore window](https://supabase.com/docs/guides/platform/upgrading)
- [Supabase dashboard](https://supabase.com/dashboard/projects)

## What this does not do

- It cannot unpause a project. Once paused, only the dashboard can restore it; the workflow reports HTTP 540 and fails so you notice.
- It is not a promise. Supabase does not publish a precise activity threshold and may change how it is measured. One request a day through the REST API is the common working practice.
- GitHub schedules are best effort: runs can be delayed, only run on the default branch, and in a public repository they are disabled after 60 days without a push. Run it daily, not weekly, and push a commit now and then.
- pg_cron inside the database cannot do this job: when the project is paused, Postgres is not running.

## Sources

- [GitHub: schedule trigger](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule)
- [Supabase FAQ: Do projects pause when they are inactive?](https://supabase.com/kb/faqs/platform/free-project-pausing)

## If you would rather not babysit it

Scribase scales idle projects to zero and wakes them on the next request instead of pausing them, so there is nothing to keep alive and nothing to restore.

Your first project is free with no time limit and no card. Existing supabase-js code works after swapping the URL and key.

## Sources

- [Scribase pricing](https://scribase.com/pricing)

[Start a free project](https://console.scribase.com/signup?ref=keepalive-page)

## FAQ

**How long before Supabase pauses a free project?**

After about 7 days with low or no activity. Supabase emails you about a week before and again when it happens. Paid plans are not paused for inactivity.

**Does this workflow count as activity?**

It makes a real database read through the project REST API, which is the kind of request Supabase describes as activity. The exact threshold is not published, so run it daily to keep a margin inside the 7-day window.

**What does HTTP 540 mean?**

The project is paused and cannot process requests until the owner restores it from the dashboard. The workflow stops retrying on 540, fails the run, and puts the restore step in the job summary.

**Why is the whole script inline instead of a uses: line?**

So the workflow has no outside dependency: nothing to version, nothing to update, nothing that can be removed from under you. You can read every line it runs.

**Can I keep several projects alive from one repository?**

Yes. Copy the step once per project with its own pair of secrets, or add a matrix over the secret names. Each project needs its own daily read.

## Related

- [Supabase project paused: full fix guide](https://scribase.com/supabase-project-paused)
- [Export a Supabase database with pg_dump](https://scribase.com/supabase-pg-dump)
- [Migrate from Supabase](https://scribase.com/migrate/supabase)
- [Pricing](https://scribase.com/pricing)