Fix
pg_restore: role "app_user" does not exist
Your data is in the dump. What is missing is everything around it: the roles that own your tables and appear in every GRANT.
The error
pg_restore: error: could not execute query: ERROR: role "app_user" does not exist
pg_restore: error: Command was: ALTER TABLE public.orders OWNER TO app_user;
From a plain SQL dump replayed with psql, the same thing reads:
ERROR: role "app_user" does not exist
On Postgres 13 and older the wording is pg_restore: [archiver (db)] Error while PROCESSING TOC, but the cause is identical.
Why it happens
A database dump describes objects and their ownership. pg_dump emits an ALTER ... OWNER TO for every table, sequence, view and function, plus the GRANT/REVOKE statements attached to them. Those statements name roles — but roles live at the cluster level, not inside a database, so pg_dump never carries them.
Restore into a fresh cluster and every one of those role names is unknown. This is why you see the error dozens of times in a row: once per object.
It bites hardest when you move a dump off a managed host. A Supabase dump, for example, references anon, authenticated, service_role and supabase_admin — none of which exist in a stock postgres:17 container.
Fix 1 — drop the ownership, if you only want the data
For a restore drill, a staging copy, or a local clone, you do not need the original roles. Tell pg_restore to skip both ownership and privileges:
pg_restore --no-owner --no-acl -d mydb backup.dump
Both flags matter. The most common mistake is passing only --no-owner (-O): ownership statements disappear, then the restore still fails on the GRANT SELECT ON ... TO app_user lines. --no-acl (an alias of --no-privileges, short form -x) removes those too.
Everything then ends up owned by the role you connect as. The same flags exist at dump time if you would rather bake it in:
pg_dump -Fc --no-owner --no-privileges -d "$SOURCE_URL" -f backup.dump
For a plain SQL dump the equivalent restore is psql -v ON_ERROR_STOP=1 -f dump.sql — you must have used the flags at dump time, since the statements are already written into the file.
Fix 2 — create the roles first, if you want a faithful copy
If the restore is a real recovery and permissions must survive, create the roles before restoring. Ownership only requires that the role exists; it does not need a password or login rights:
psql -d mydb -c 'CREATE ROLE app_user NOLOGIN;'
psql -d mydb -c 'CREATE ROLE readonly NOLOGIN;'
To capture them all from the source cluster instead of guessing, dump the globals — roles and tablespaces, no table data:
pg_dumpall --globals-only -d "$SOURCE_URL" -f globals.sql
psql -d postgres -f globals.sql
Two caveats on managed Postgres. Role passwords come from pg_authid, which most managed hosts do not let you read, so they will be missing from globals.sql. And the file usually contains attributes your target refuses (SUPERUSER, REPLICATION, provider-internal roles) — read it before replaying it, and strip what does not apply.
Which role names do you actually need?
Ask the dump, before the restore fails:
# every owner recorded in the archive's table of contents
pg_restore -l backup.dump | grep -oE '[^ ]+$' | sort -u | head -20
Or list the source cluster directly:
psql -d "$SOURCE_URL" -c '\du'
The part that actually hurts
By default pg_restore does not stop on these errors. It logs each one, keeps going, and exits with a non-zero status at the end — which a cron job piping to /dev/null will happily ignore. The tables and rows land, so the restore looks fine; the GRANTs did not, so your application role can no longer read the tables it owns.
Make failures loud:
# stop at the first error instead of limping to the end
pg_restore --exit-on-error --no-owner --no-acl -d mydb backup.dump
# plain SQL equivalent
psql -v ON_ERROR_STOP=1 --single-transaction -d mydb -f dump.sql
Then verify the permissions came back, not just the rows — \dp in psql shows the access privileges of each table. A restore that ends with correct row counts and empty ACLs is exactly the kind of half-success the drill is designed to catch.
Restores that check more than row counts
pgProof restores every backup into a disposable container and verifies what actually matters: counts against a manifest, foreign keys, RLS policies, and your own probe queries. A dump that restores without its privileges fails the drill the same day it is taken.
Join the early-access waitlist