How to back up Supabase Storage files
Supabase Storage files need their own backup. Database backups, including PITR, capture the rows that describe your uploads but not the uploads themselves, because the files sit in an S3-compatible object store rather than in Postgres. Restore only the database and you get a complete-looking catalogue where every download returns an error.
Use the S3-compatible endpoint
Supabase Storage exposes an S3-compatible endpoint, so ordinary S3 tooling works. In Storage > Configuration > S3, enable the protocol and create an access-key pair. The secret is shown once. Use a dedicated pair for the backup job, keep it out of the script and delete it when the job no longer needs it.
The connection steps below follow the official Supabase Storage download guide. Copy the endpoint and region from the same S3 configuration screen instead of constructing them from the project reference.
export AWS_ACCESS_KEY_ID='<access-key-id>'
export AWS_SECRET_ACCESS_KEY='<secret-access-key>'
export AWS_DEFAULT_REGION='<storage-region>'aws s3api list-buckets --endpoint-url '<storage-endpoint-url>'aws s3 sync 's3://<bucket>' './backup/<bucket>' --endpoint-url '<storage-endpoint-url>' --no-progressRun the sync once per bucket. It is incremental, which makes repeated copies practical, but it is not a point-in-time snapshot: objects can change while the command is running. Record the start and finish time and coordinate a quiet window when the database and files must line up exactly. Do not add --delete to a backup destination unless that destination has versioning or another retention layer.
aws s3 ls 's3://<bucket>/' --endpoint-url '<storage-endpoint-url>' --recursive --summarizeBucket settings are not files
Copying the bytes is not the whole job. Public and private flags, size limits, allowed MIME types and the access policies that protect the objects all live outside the object store. Record them alongside the files, or a restore will produce the right bytes behind the wrong permissions.
Check the copy is complete
A sync that reports success is not proof of a complete copy. Two checks catch most problems:
- Compare the object count in the backup with the row count in the Storage catalogue. A gap means files the database expects are missing.
- Hash every copied object and store the path, byte count and digest in a manifest. Sampling can miss the one file needed during recovery.
ReviveDB hashes every captured object, not a sample. While an object streams from Supabase, its byte count and SHA-256 digest are measured. The uploaded staging object is hashed again, and a newly promoted content-addressed blob is checked once more. A reused object is not blindly trusted either: its stored bytes are rehashed before reuse.
The complete path is visible in the Storage backup implementation and its streaming and reuse tests. This design catches a more important failure mode than a successful HTTP response: bytes changing size or ETag while a backup is in progress.
Keep the two backups aligned
The database and Storage are copied by separate processes, so they can drift apart. If the database is from Tuesday and the files are from Friday, a restore produces rows pointing at files that did not exist yet, and files with no rows to reference them. Run both from one scheduled job, or accept the gap deliberately and write down how large it can get.
This is why ReviveDB groups a run into one recovery point. Background in do Supabase backups include Storage files and what a recovery point contains.