There are a few known issues that may be blocking in some setups, but I'm working on getting those resolved as quickly as possible. If you run into anything else, definitely let me know. And feel free to open an issue on GitHub if you find something that isn't already tracked.
Yeah, this is a good distinction. pgDumpster already tries pretty hard not to treat “the restore command exited successfully” as proof that the recovered project is actually healthy, especially because DB/Auth/Storage/control-plane state don't share one atomic consistency boundary. But you're right that application-level authorization deserves an explicit callout in the docs. Row counts, checksums and schema parity can't tell you whether an authenticated user can suddenly see another user's rows. I'll add a post-restore verification section specifically calling out testing with anon/authenticated users and checking auth-linked application invariants/FKs. I'd like to automate more of that eventually too, but there's a limit to what pgDumpster can generically prove about application-specific RLS semantics without knowing what the app considers valid access.
Thanks! Appreciate it
Appreciate it! If you run into anything weird, please open an issue. Real-world Supabase setups are exactly what I need to find the edge cases I haven't hit yet
Oh, that's really useful. I may have been too conservative on that one. I was treating provider secrets as `not_exportable` because I hadn't found a reliable way to get the original secret material back. The Management API schema does actually expose fields like `external_google_secret`, so if your gist is getting the real values rather than redacted placeholders, that's definitely something I need to test. pgDumpster already treats secret-bearing backup material separately and encrypts it, so if those values are actually recoverable this should probably be backed up rather than marked as a manual restore item. Thanks for sharing this, I'll test it against a disposable project.
That makes sense. Special-casing the core Supabase schemas and treating everything else as normal Postgres state is probably the pragmatic approach for a migration tool. The Stripe case was exactly what made me stop doing that blindly though. Some of those schemas look like normal database state until you inspect what actually lives in them, and then you suddenly have webhook secrets, sync bookkeeping, migration state etc. Restoring the rows is easy. But knowing whether the external integration is actually valid afterwards is the harder part. Asset URLs are a really good one too. I hadn't considered arbitrary application columns containing hardcoded Supabase Storage URLs. The files can be restored perfectly and the app can still point at the old project. And yeah, I completely agree with your last point. The deeper I get into this, the less useful it is to think of Supabase as "Postgres with some extras". There's a surprising amount of platform state around the database. I'll give homebox a proper look and drop you a star too.
Funny you mention extensions. I actually ran straight into that issue while testing after i released v0.1.2 Stripe Sync Engine had created its own persistent `stripe` schema, and pgDumpster deliberately refused to continue because it couldn't safely classify that state yet. That's already tracked as issue #3, and issue #4 is now about explicit schema/table exclusions while still marking the backup as intentionally incomplete rather than pretending coverage is 100%. The Storage load caveat is a good point too, especially once object counts get large. And yeah, agreed on larger DBs. I'm trying very hard not to reinvent PostgreSQL backup tooling where the existing tooling already does the job better.
Yep, I ran into both of those too. For the auth provider secrets I couldn't find a proper way around it either. Supabase gives you enough of the auth config to know something like Google OAuth is configured, but not necessarily the original secret material. I'm treating those as explicitly `not_exportable` / manual restore items rather than pretending the backup is complete. Same with the database password. As far as I can tell Supabase simply doesn't expose the existing DB password through the Management API, so for anything that needs direct Postgres access I require that separately. One thing I'd actually be curious about with your approach though. How are you handling Supabase integrations that create their own persistent database state? I just ran into this with the Stripe Sync Engine. It creates a `stripe` schema with things like `_migrations`, `_managed_webhooks`, `_sync_runs`, etc., and some of that is clearly integration-owned state rather than just normal application tables. When you decompose a hosted project into the individual OSS services, do you just migrate that schema as normal Postgres state, recreate the Stripe integration separately, or do you have special handling for stuff like that? That's one of the areas where I've been pretty conservative, because restoring the tables byte-for-byte doesn't necessarily mean you've actually restored the integration correctly. Your cloud to self-hosted decomposition is really interesting though. I'll definitely have a look at homebox.
Give me a couple of days. I've decided to build exactly this now. Complete Supabase backups: Postgres including auth/roles/schema/data, actual Storage objects, Edge Functions/config where recoverable, encryption, integrity verification, automated backup jobs, and a documented/tested restore flow. It'll be publicly available and free to use for normal/self-hosted use. I'm licensing it as source-available rather than OSI open-source, mainly to prevent someone from just wrapping it into a competing hosted backup service. I'll post it here when it's ready.
You can do this without relying on a open-source project. Supabase already gives you everything you need. * `supabase db dump` for roles, schema and data * Supabase Storage exposes an S3-compatible endpoint * `rclone` can copy the actual Storage objects to another S3-compatible target like R2 * run both from GitHub Actions or cron So you could do: `supabase db dump` \-> upload dumps to R2 `rclone sync` \-> Supabase Storage -> R2 If you need any help, feel free to reach out.
This is pretty much directly in my wheelhouse. React/TypeScript + Supabase is the stack I work with, and I also come from an IT operations background so the production/stability side is something I’m very comfortable with. Sent you a DM.
Yep, those errors pretty much confirm it. `42P06` means the dump is trying to create `public` even though it already exists, and the `42501` on `auth` means it is trying to restore into Supabase's managed auth schema. The dump is simply too broad. Export only the schema/tables you actually created, or use the Supabase CLI dump which filters out Supabase-managed schemas. If your own tables are in `public`, include those tables and their definitions, but don't try to recreate the `public` schema itself or restore Supabase-managed schemas like `auth` and `storage`.
A fresh Supabase project is not actually an empty Postgres database. Supabase already creates things like `auth`, `storage`, roles, extensions etc. So if your pgAdmin dump also contains those, you're going to get conflicts. I would export only the schema you actually own, and migrate that instead of restoring the entire database. If you post the exact error and how you made the dump in pgAdmin, it should be pretty obvious what is colliding.
The compromised account / deleted project point is probably the strongest argument for it. I had not really considered the fact that your production data and Supabase backups are still sitting behind the same account/provider boundary. In that case, having an independent copy elsewhere is definitely valuable. The restore testing point is fair too, although I’m probably a bit less worried about that one. If I’m running an actual production database with important data, I’d consider testing the restore process part of operating it properly anyway. Automating that verification is still useful though. I’m less convinced by the billing lapse example, since Supabase doesn’t just immediately wipe the project if a payment fails, but I get the broader point. As for Free users, I honestly think that’s a difficult market. Personally, if the data matters enough that I’m willing to pay $12/month purely to protect it, I probably shouldn’t be running the production environment on Supabase Free in the first place. For a Free user I’d probably expect something more in the $2-4/month range. Not because backups are only worth that, but because once the price gets anywhere near half of Pro, I’d rather just move to Pro. So I think you’re right that the stronger positioning is probably “independent second backup layer for production Supabase projects” rather than “backups for Free users”.
I like the idea, especially since backups are one of the obvious limitations of the Supabase free tier. But I’m struggling a bit with the pricing. If I’m on Supabase Free and willing to pay $12/month just for backups, Supabase Pro is only another $13/month and already gives me daily database backups with 7-day retention, while also increasing basically every other limit on the project. I get that your product also covers Storage and gives you an independent/off-site backup with restore verification, which is genuinely useful. But that almost makes me think the target customer is someone already on Pro who wants a second backup layer, rather than someone trying to add backups to Free. At $12/month, who do you actually see as the main customer?
Personally, I would prefer the extra manual setup if the integration is handling production data. It adds a little friction during onboarding, but the permission boundary becomes much clearer, as the service only ever receives the exact database access it needs. For less sensitive projects, I can understand why some users would choose the OAuth flow for convenience.
The temporary role itself sounds properly restricted, but I think the real trust boundary is still the OAuth token. `database:write` allows the integration to execute SQL, so from the customer's perspective they are still authorizing significantly more than read-only access, regardless of what role is created afterwards. I would prefer a one-time setup where the customer creates a dedicated backup role manually and only shares that restricted credential. It is slightly less convenient, but the permission shown during onboarding would then match the actual access being granted. At minimum, I would describe this as OAuth-managed provisioning of read-only access rather than read-only OAuth access.
The combination of interval polling, `router.refresh()`, N+1 queries and multiple Realtime channels can absolutely multiply requests even with only a few users. I’d first check exactly which Supabase usage metric is hitting 100%. My main suspect would be the full page refresh every five minutes combined with server components re-fetching everything and the relation queries running in loops. I work with Next.js and Supabase regularly. Feel free to message me if you want another pair of eyes on the architecture.
I wouldn’t put every write behind an Edge Function just because the anon key is public. That key being extractable is expected. RLS is the actual security boundary. For a new app, i would do it like this: * Create an anonymous Supabase user on first launch, so each install gets its own user ID/JWT. * Allow simple low-risk actions like likes directly through RLS, with unique constraints to prevent duplicate likes. * Use Edge Functions for comments and reports, where you may want validation, rate limiting, spam checks and moderation. * Keep RLS enabled even when using Edge Functions. I wouldn’t use the service role for normal user writes unless it is genuinely needed, since it bypasses RLS. Nothing can completely prevent someone from extracting the app and calling the API directly. You can only make abuse harder and easier to detect: rate-limit by user and IP, add cooldowns and payload limits, log suspicious activity, and possibly use Play Integrity/App Attest. Device IDs can be spoofed, and IP addresses are noisy, so I wouldn’t rely on either one alone. So personally i would do, direct RLS writes for likes, Edge Functions for comments/reports, and anonymous Auth rather than treating every installation as the same `anon` user.
With self-hosted Supabase, the Auth/SMTP configuration generally needs to be set in your `.env` or `docker-compose.yml` rather than the Studio settings page. After updating `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, etc., restart the auth container. If you already tried that, check docker compose logs auth --tail=100 And check the failed request in the browsers devtool network tab. The Studio notification is generic, but the response status and auth container logs should show the actual cause.
I mostly test the policies directly rather than testing every API endpoint. For multi-tenant stuff I create users in two different tenants, authenticate as one of them, and then try the obvious bad cases: reading the other tenant's rows, updating them, inserting with the wrong tenant ID, or changing an existing row's tenant ID. I run those tests against the actual database migrations, usually with pgTAP. The areas I worry more about are the places that can bypass RLS entirely: service role queries, security definer functions, RPCs and server-side code. Also, swapping an ID in an API request should not expose anything if the request reaches Postgres with the user's JWT and the RLS policy is correct. If it does, something is probably bypassing RLS somewhere.
I think the main issue is that your Broadcast configuration doesn't match what `realtime.broadcast_changes()` expects. `realtime.broadcast_changes()` uses a private channel with Realtime Authorization, but your client is explicitly subscribing with: config: { private: false } I'd try changing the setup to an authenticated/private channel: create policy "authenticated can receive broadcasts" on realtime.messages for select to authenticated using (true); Then client-side: await supabaseClient.realtime.setAuth(); const changes = supabaseClient .channel(`topic:${activeJob.jobId}`, { config: { private: true } }) .on("broadcast", { event: "INSERT" }, (payload) => console.log("INSERT", payload) ) .on("broadcast", { event: "UPDATE" }, (payload) => console.log("UPDATE", payload) ) .subscribe((status, error) => { console.log(status, error); }); One other thing I'd double-check: your trigger broadcasts to: 'topic:' || NEW.id::text so `activeJob.jobId` has to be the exact same value as `NEW.id`. If `jobId` refers to some other column, you'll successfully subscribe to a channel but never receive the trigger's broadcasts. If you intentionally want this to be public/unauthenticated, I'd probably use `realtime.send(..., false)` instead, since `broadcast_changes()` is designed around the private/authorized database-change flow.