SafeGrd / Neon backup

How to back up a Neon database, and keep the copy outside Neon

Neon is PostgreSQL with branching and a history you can restore from, which covers most of what a backup is for day to day. It does not cover a project that is deleted, a history window that has passed, or a copy you need to hand to someone outside Neon. Neon's own documentation says to automate pg_dump to remote storage for those. This page is that: the connection string that works, the command, where the file goes, the restore, and the check. The last section is how SafeGrd runs it with no server of your own.

What Neon's restore covers, and what it does not

From Neon's plans page, read on 2026-10-08:

PlanHistory windowSnapshots
FreeUp to 6 hours, capped at 1 GB of change history1 manual; no scheduled backups
LaunchUp to 7 days, billed per GB-month of history100 manual, plus scheduled backups
ScaleUp to 30 days, billed per GB-month of history100 manual, plus scheduled backups

1. The connection string

In the Neon console, the Connect panel shows a string with a Connection pooling toggle. Turn it off for a backup, so the host does not end in -pooler:

postgresql://neondb_owner:PASSWORD@ep-example-123456.eu-central-1.aws.neon.tech/neondb?sslmode=require

2. Make a read-only role

neondb_owner can write. Back up as a role that cannot, so a leaked backup credential changes nothing. Run this in the SQL editor:

CREATE ROLE backup LOGIN PASSWORD 'a long random password';
GRANT pg_read_all_data TO backup;

Neon runs PostgreSQL 14 and later, so pg_read_all_data is available and covers tables made after the grant. A table with row-level security dumps only the rows its policies show this role; give it BYPASSRLS if you use RLS.

3. Dump it

pg_dump --format=custom --no-owner --no-privileges --file=neondb.dump "$DATABASE_URL"

Neon's guide adds -v to watch it. Match the client to the project's major version: an older pg_dump refuses a newer server. One dump covers one database on one branch, so a project with several databases, or a branch you also want, is one command each. --no-owner --no-privileges keeps Neon's role names out of the dump, which is what you want when the restore target is not Neon.

4. Encrypt it, ship it, schedule it

A dump holds every row in the clear. Encrypt it on the way out and write it to a bucket in an account that is not Neon's and not the application's:

set -o pipefail
pg_dump -Fc --no-owner --no-privileges "$DATABASE_URL" | age -r "$AGE_RECIPIENT" | aws s3 cp - "s3://$BUCKET/neon/$(date -u +%F).dump.age"

Neon has no server to put a cron job on, so Neon's own guide runs this from a scheduled GitHub Actions workflow; the Supabase page has a 10-line workflow that is the same for Neon with the direct string in the secret. set -o pipefail makes a failed dump fail the job instead of uploading a truncated file. The bucket policy, a key that cannot delete and the lock on the bucket are in the S3 guide and the Object Lock guide.

A dump reads the whole database every night, and Neon meters egress after the plan's allowance (5 GB on Free, 500 GB on Launch and Scale, from the same page). safegrd estimate prints what a nightly, weekly or hourly schedule would move for your database (estimate).

5. Restore

Into Neon: make a new project or a new database, with the same name as the one dumped, and restore over its direct string. Into anything else: create an empty database on any PostgreSQL of the same or a newer major version.

aws s3 cp "s3://$BUCKET/neon/2026-10-08.dump.age" - | age -d -i backup-key.txt > neondb.dump
pg_restore --exit-on-error --no-owner --no-privileges --dbname "$RESTORE_URL" neondb.dump

Use a direct string for the restore as well. --exit-on-error stops at the first failure instead of carrying on and reporting success; --jobs=4 restores four tables at once, from a file, for a large database. The dump says CREATE EXTENSION for each extension the database used; a target that lacks one fails there, so install it first.

6. Check what came back

Compare each table's row count in the restored database with the source, as How to test that a backup restores shows, and do it on a schedule.

How SafeGrd does it

SafeGrd takes the backup from a scheduled GitHub Actions job, or on a machine it starts for each backup from the connection string, over the direct endpoint, and encrypts it before upload. Each backup uploads only what changed since the last one; the read is still the whole database, so Neon's egress is about the database's size per backup, which safegrd estimate prints. The drill restores into a throwaway PostgreSQL of the same major version, and the recovery document beside each snapshot lists the extensions the database had, so a restore target can be prepared before it is needed. SafeGrd does the same job on every platform; the PostgreSQL page has what it dumps, how it encrypts, and what a drill checks.

Run your first Fire Drill The GitHub Actions setup

- uses: safegrd/backup-action@v0
  with:
    config: ${{ secrets.SAFEGRD_CONFIG }}
    database-url: ${{ secrets.DATABASE_URL }}   # the direct string, no -pooler

The setup is the Supabase guide's with Neon's direct string in DATABASE_URL. With no workflow at all, Back up on SafeGrd under Surfaces in the console takes the same string; a machine SafeGrd starts for each backup dumps and encrypts it, then is destroyed, and the string is sealed and given only to that machine (how).

Questions

Does this replace Neon's instant restore?

No. Instant restore takes the branch back to any second in its window, which a dump cannot. The dump is the copy for after the window, after the project, and for anywhere that is not Neon.

Which branch?

Whichever the connection string names. The main branch is the usual one. A dump of a child branch is a dump of that branch's data as it is, not of its parent.

Related