<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: SuperLede</title>
    <description>The latest articles on DEV Community by SuperLede (@superlede).</description>
    <link>https://dev.to/superlede</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4027485%2Fc4e05bf5-3771-4db9-839a-ce58ee7f8e11.png</url>
      <title>DEV Community: SuperLede</title>
      <link>https://dev.to/superlede</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/superlede"/>
    <language>en</language>
    <item>
      <title>I checked 1,000 YouTube channels to see which ones look monetized. Here's what the data says, and what it can't.</title>
      <dc:creator>SuperLede</dc:creator>
      <pubDate>Sat, 05 Sep 2026 16:59:36 +0000</pubDate>
      <link>https://dev.to/superlede/i-checked-1000-youtube-channels-to-see-which-ones-look-monetized-heres-what-the-data-says-and-4k0j</link>
      <guid>https://dev.to/superlede/i-checked-1000-youtube-channels-to-see-which-ones-look-monetized-heres-what-the-data-says-and-4k0j</guid>
      <description>&lt;p&gt;In November 2023 YouTube stopped exposing a public flag that said whether a channel was in the Partner Program. Every tool that answered "is this channel monetized" was reading that flag. They all still answer the question. None of them will tell you they're now guessing.&lt;/p&gt;

&lt;p&gt;I built one of those tools, so I want to lay out what mine actually reads, where it fails, and what happened when I pointed it at 1,000 channels across 30 countries.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can and can't see from outside
&lt;/h2&gt;

&lt;p&gt;"Monetized" means the channel is in the Partner Program. There are two tiers:&lt;/p&gt;

&lt;p&gt;Fan funding needs 500 subscribers, 3 public uploads in 90 days, and either 3,000 watch hours in a year or 3M Shorts views in 90 days. It unlocks memberships, Super Thanks and the merch shelf.&lt;/p&gt;

&lt;p&gt;Ad revenue needs 1,000 subscribers and either 4,000 watch hours in a year or 10M Shorts views in 90 days.&lt;/p&gt;

&lt;p&gt;Watch hours are private. Subscriber count usually isn't. So from outside you can see the eligibility line but not whether the creator applied, got in, or got kicked out later. Anyone quoting an accuracy percentage is quoting a number they have no way to compute.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five things I read
&lt;/h2&gt;

&lt;p&gt;Every check returns these five as pass, fail or unknown, and the result page shows all five with a sentence each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subscriber count&lt;/strong&gt;, from the official API. It's a clue about eligibility, nothing more. A 40,000-subscriber channel can be unmonetized and a 900-subscriber one can be in the fan funding tier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Join button.&lt;/strong&gt; Memberships are Partner Program only, so if it's there the channel is in. If it's missing that means almost nothing, because plenty of monetized channels never turn memberships on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ads on recent long-form uploads.&lt;/strong&gt; I sample several recent long videos and check whether ads are actually being served. Strongest signal for the ad revenue tier, with a caveat I'll get to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Merch shelf.&lt;/strong&gt; Also Partner Program only, also opt-in, also needs a linked store. Present is strong evidence, absent is normal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 500-subscriber floor.&lt;/strong&gt; Below it the channel can't be in the program at all. This is the one signal whose failure is a real negative.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap
&lt;/h2&gt;

&lt;p&gt;Since November 2020, YouTube runs ads on videos from channels that are not in the Partner Program. The creator gets nothing from those ads.&lt;/p&gt;

&lt;p&gt;So "I saw a pre-roll" proves nothing on its own, and any checker that only looks for ads will confidently call an unmonetized channel monetized. I require ads on at least two thirds of the videos I successfully sampled, and a channel with one lucky pre-roll doesn't clear that.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the verdict comes out
&lt;/h2&gt;

&lt;p&gt;Evidence has an order. Partner-Program-only features first, then ad delivery, then subscriber counts. Subscriber counts never override a product signal: if a channel shows a Join button at 480 subscribers, the Join button wins.&lt;/p&gt;

&lt;p&gt;Four outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Likely monetized&lt;/strong&gt; — enough subscribers, memberships on, ads running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Possibly monetized&lt;/strong&gt; — one Partner Program feature or the ad signal is there, but the set isn't complete. Usually memberships on, ads not detected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Likely not monetized&lt;/strong&gt; — no positive signal at all, and either under 500 subscribers or clearing 1,000 with every readable product signal negative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can't tell&lt;/strong&gt; — the evidence is missing or points both ways. I say so instead of picking.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two rules that took the longest to get right
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Unknown is not fail.&lt;/strong&gt; A blocked fetch, a hidden subscriber count, a channel with no long-form uploads: none of those are evidence of anything. They stay unknown and never push a verdict toward "not monetized". My first version got this wrong and produced confident false negatives for channels that were merely hard to read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failed samples stay out of the denominator.&lt;/strong&gt; If I try five videos and one loads, "1 of 1 has ads" is not a 100% ad rate, it's a sample of one. Below two successful fetches the ad signal stays unknown.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 1,000 channels looked like
&lt;/h2&gt;

&lt;p&gt;Guessing about aggregate behaviour is easy, so I measured it. For each of 30 countries I asked the API for the most popular videos right now, took the channels behind them, and checked 35 per country.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;Look monetized&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Greece, Hungary&lt;/td&gt;
&lt;td&gt;51%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brazil, Romania&lt;/td&gt;
&lt;td&gt;49%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;France&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Germany, Thailand&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UK&lt;/td&gt;
&lt;td&gt;26%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US&lt;/td&gt;
&lt;td&gt;23%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Japan&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Korea&lt;/td&gt;
&lt;td&gt;9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;India&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things surprised me. The US is nowhere near the top, despite being the market everyone assumes is most monetized. And the spread is enormous for channels that are all, by construction, big enough to be trending.&lt;/p&gt;

&lt;p&gt;I can't fully explain the US number from outside, which is the honest answer. My guess is that US trending skews to music and network content where memberships and merch simply aren't used, but I can't prove that, so it stays a guess in public too.&lt;/p&gt;

&lt;p&gt;The other number worth stating: I couldn't tell either way for 18% of channels. That's the number a tool selling certainty would quietly drop.&lt;/p&gt;

&lt;p&gt;Full table, method and the CSV are here: &lt;a href="https://monetizationkit.com/data/youtube-monetized-channels-by-country/" rel="noopener noreferrer"&gt;https://monetizationkit.com/data/youtube-monetized-channels-by-country/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One engineering note, and a bug worth stealing
&lt;/h2&gt;

&lt;p&gt;The first version read video data from YouTube's internal player endpoint. That's now gated by proof-of-origin tokens for automated callers, so it reads the public watch page instead. Slower, messier, doesn't pretend to be a browser.&lt;/p&gt;

&lt;p&gt;The bug I'd point at, because I suspect it's common: I had a rule saying a forced refresh must not overwrite good cached data with a degraded result. Sensible. But it also blocked the very first write for a channel that had never been cached, so every channel whose signals I couldn't read was silently dropped. The dataset could only ever show three of the four verdicts, and it made the tool look more confident than it was. Data that never gets written is much harder to notice than data that's wrong.&lt;/p&gt;

&lt;p&gt;The second one, same flavour: my collection run showed ad-fetch failures jumping from 1.2% in the first hour to 30% in the second. I read that as "these channels are unreadable" and nearly published it. It was my own rate limiting. Slowing down and re-running recovered nine in ten. If you scrape for a dataset, plot your failure rate against time before you believe any of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd like torn apart
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Is there a public signal I'm missing? Super Thanks and Super Chat are Partner Program features too. I surface them where visible but don't score them.&lt;/li&gt;
&lt;li&gt;Is one middle tier clearer than two for non-technical creators, or is "possibly" doing too much work?&lt;/li&gt;
&lt;li&gt;If you run a channel: does the breakdown match what Studio shows you? That's the only real validation this can get.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The checker is free and needs no login: &lt;a href="https://monetizationkit.com/" rel="noopener noreferrer"&gt;https://monetizationkit.com/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>youtube</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Supabase Backups Don't Include Your Storage Files. Here's What Does.</title>
      <dc:creator>SuperLede</dc:creator>
      <pubDate>Wed, 15 Jul 2026 17:20:39 +0000</pubDate>
      <link>https://dev.to/superlede/supabase-backups-dont-include-your-storage-files-heres-what-does-14e9</link>
      <guid>https://dev.to/superlede/supabase-backups-dont-include-your-storage-files-heres-what-does-14e9</guid>
      <description>&lt;p&gt;The restore finished clean. &lt;code&gt;pg_restore&lt;/code&gt; exited 0, every table came back, row counts matched, the app booted. Twenty minutes later the first support message arrived: profile pictures were broken. Then the PDFs. Then every file anyone had ever uploaded.&lt;/p&gt;

&lt;p&gt;Nothing about the restore failed. This is how Supabase Storage is built — and it applies equally to the built-in daily backups, to PITR, and to your own &lt;code&gt;pg_dump&lt;/code&gt;. None of them include your Storage files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your database only stores pointers
&lt;/h2&gt;

&lt;p&gt;Supabase Storage is two systems. The &lt;code&gt;storage.objects&lt;/code&gt; table in your Postgres database holds the &lt;strong&gt;metadata&lt;/strong&gt; — bucket, path, timestamps, owner. The actual file bytes live in a separate S3 backend operated by Supabase, entirely outside your database.&lt;/p&gt;

&lt;p&gt;Restoring the database restores the pointers. If the files behind them were deleted, or you're restoring into a fresh project, those pointers now reference objects that don't exist. The dangerous part: everything looks healthy from SQL. Row counts match, constraints hold, &lt;code&gt;select count(*) from storage.objects&lt;/code&gt; is exactly right — until something requests a file and gets a 404.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove it to yourself in five minutes
&lt;/h2&gt;

&lt;p&gt;Ask your database what it thinks is in Storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;bucket_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;
&lt;span class="k"&gt;limit&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now dump your database with &lt;code&gt;pg_dump&lt;/code&gt;, restore it into a local Postgres, and run the same query. Every row is there. Not one of the files those rows describe came with the dump. The restore passes every SQL check you can write and still lost your users' data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backing the files up
&lt;/h2&gt;

&lt;p&gt;The good news: Supabase exposes the Storage backend over an S3-compatible endpoint, so the files can be copied out like any other bucket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, create S3 credentials.&lt;/strong&gt; Dashboard → &lt;strong&gt;Storage → Settings → S3 Access Keys&lt;/strong&gt; → enable the S3 connection and generate a key pair. The endpoint and region are shown on the same page; the endpoint looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://&amp;lt;project-ref&amp;gt;.storage.supabase.co/storage/v1/s3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One warning before anything else: &lt;strong&gt;these keys bypass RLS.&lt;/strong&gt; Treat them like a service-role key — server-side only, never in client code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: DIY with rclone
&lt;/h3&gt;

&lt;p&gt;Sync every bucket to storage you own — Cloudflare R2, Backblaze B2, or your own S3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.config/rclone/rclone.conf
&lt;/span&gt;&lt;span class="nn"&gt;[supabase]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;s3&lt;/span&gt;
&lt;span class="py"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Other&lt;/span&gt;
&lt;span class="py"&gt;access_key_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;SUPABASE_S3_ACCESS_KEY_ID&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;secret_access_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;SUPABASE_S3_SECRET&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;https://&amp;lt;project-ref&amp;gt;.storage.supabase.co/storage/v1/s3&lt;/span&gt;
&lt;span class="py"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;project-region&amp;gt;&lt;/span&gt;

&lt;span class="nn"&gt;[r2]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;s3&lt;/span&gt;
&lt;span class="py"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Cloudflare&lt;/span&gt;
&lt;span class="py"&gt;access_key_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;R2_ACCESS_KEY_ID&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;secret_access_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;R2_SECRET&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;https://&amp;lt;account-id&amp;gt;.r2.cloudflarestorage.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rclone &lt;span class="nb"&gt;sync &lt;/span&gt;supabase:&amp;lt;bucket&amp;gt; r2:my-backups/storage/&amp;lt;bucket&amp;gt; &lt;span class="nt"&gt;--checksum&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If rclone throws signature errors against the Supabase endpoint (some releases after 1.67.0 did), the plain AWS CLI is a reliable fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 &lt;span class="nb"&gt;sync &lt;/span&gt;s3://&amp;lt;bucket&amp;gt; ./storage-backup/&amp;lt;bucket&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; &lt;span class="s2"&gt;"https://&amp;lt;project-ref&amp;gt;.storage.supabase.co/storage/v1/s3"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;project-region&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to get right if you go this route:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dump the database in the same run.&lt;/strong&gt; Schedule the &lt;code&gt;rclone sync&lt;/code&gt; and the &lt;code&gt;pg_dump&lt;/code&gt; together (cron, or a GitHub Actions workflow), so the database snapshot and the file snapshot describe the same moment. Otherwise a restore hands you metadata rows and files that disagree with each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget for egress.&lt;/strong&gt; Downloading your files counts as uncached egress on your Supabase bill — $0.09/GB beyond your plan's allowance (Free: 5 GB/month, enforced through a fair-use flow of notice → grace period → restrictions, so recurring large backups aren't viable there; Pro: 250 GB included). For big buckets, weekly beats daily.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Option B: an open-source CLI that does both in one run
&lt;/h3&gt;

&lt;p&gt;Disclosure up front: I'm the author. The backupdrill CLI (MIT, &lt;a href="https://github.com/backupdrill/cli" rel="noopener noreferrer"&gt;github.com/backupdrill/cli&lt;/a&gt;) grew out of exactly the incident at the top of this post. One run streams the &lt;code&gt;pg_dump&lt;/code&gt; (custom format) &lt;strong&gt;and&lt;/strong&gt; the Storage files to a bucket you own, and writes a manifest with SHA-256 checksums:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; backupdrill

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BACKUPDRILL_SUPABASE_STORAGE_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;ref&amp;gt;.storage.supabase.co/storage/v1/s3"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BACKUPDRILL_SUPABASE_STORAGE_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;project-region&amp;gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BACKUPDRILL_SUPABASE_STORAGE_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BACKUPDRILL_SUPABASE_STORAGE_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;

backupdrill backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll also need the database connection string and a destination bucket — the repo covers the full configuration, ships a GitHub Actions template for scheduling, and has an &lt;code&gt;estimate&lt;/code&gt; command that projects the egress cost before you commit to a schedule.&lt;/p&gt;

&lt;p&gt;The part I actually built it for is &lt;code&gt;backupdrill drill&lt;/code&gt;: it restores the latest snapshot into a throwaway Docker Postgres and verifies it — archive checksum, table count against the manifest, populated tables restored non-empty, sampled SHA-256 checks on the Storage files — then destroys the container. Because a backup you've never restored is a guess.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option C: the hosted version
&lt;/h3&gt;

&lt;p&gt;If you'd rather not babysit cron: &lt;a href="https://backupdrill.com" rel="noopener noreferrer"&gt;BackupDrill&lt;/a&gt; runs the same open-source engine on a schedule — database and Storage files captured together into your own bucket, restore drills that prove the snapshot actually comes back, failures emailed. The free plan is one project with weekly backups and includes a restore drill on your first backup, no card.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Whatever you use for the database — built-in backups, PITR at $100 per month, or your own dumps — none of it covers Storage files. If users upload anything you'd mind losing, the files need their own backup path, and the database snapshot and file snapshot need to describe the same moment.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is an expanded version of a guide originally published on &lt;a href="https://backupdrill.com/guides/supabase-storage-backup" rel="noopener noreferrer"&gt;backupdrill.com&lt;/a&gt;. I build BackupDrill; the CLI above is MIT and works without the hosted service.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
