<?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: Siddhartha Kodali</title>
    <description>The latest articles on DEV Community by Siddhartha Kodali (@siddhujz).</description>
    <link>https://dev.to/siddhujz</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%2F4130204%2Fd8b3dbe8-8db6-40d2-8351-6566a2744b54.jpg</url>
      <title>DEV Community: Siddhartha Kodali</title>
      <link>https://dev.to/siddhujz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/siddhujz"/>
    <language>en</language>
    <item>
      <title>Your Supabase free-tier database has no backups. Here is how I fixed that in 15 minutes.</title>
      <dc:creator>Siddhartha Kodali</dc:creator>
      <pubDate>Thu, 17 Sep 2026 16:04:38 +0000</pubDate>
      <link>https://dev.to/siddhujz/your-supabase-free-tier-database-has-no-backups-here-is-how-i-fixed-that-in-15-minutes-1cb2</link>
      <guid>https://dev.to/siddhujz/your-supabase-free-tier-database-has-no-backups-here-is-how-i-fixed-that-in-15-minutes-1cb2</guid>
      <description>&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;Supabase Pro takes a daily backup and keeps it for seven days. The free tier does not take any. That is a fair deal for free hosting, but a lot of side projects that end up with real users started on the free tier, and the backup question never came up again.&lt;/p&gt;

&lt;p&gt;There is a second problem that applies to Pro as well: the backup lives inside the same platform as the database. If your account is locked, if there is a regional outage, or if you decide to move to Neon or to your own server, a backup you cannot download is not one you can restore anywhere else.&lt;/p&gt;

&lt;p&gt;The fix has been the same for twenty years: run &lt;code&gt;pg_dump&lt;/code&gt; on a schedule and write the file somewhere you control. What usually gets skipped is the part around it. You want an alert when the job silently stops running, a check that the file will actually restore, and retention that never deletes the wrong file.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://getdumpling.dev" rel="noopener noreferrer"&gt;Dumpling&lt;/a&gt; to do that and nothing else. Here is the full setup for a Supabase project. It took me about fifteen minutes the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: create a read-only database user
&lt;/h2&gt;

&lt;p&gt;Don't give a backup service your &lt;code&gt;postgres&lt;/code&gt; superuser. In the Supabase SQL editor, run:&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;dumpling&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'a-long-random-password'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CONNECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;dumpling&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;dumpling&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;dumpling&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;SEQUENCES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;dumpling&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;dumpling&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeat the three &lt;code&gt;GRANT&lt;/code&gt; lines for any other schema you want included. &lt;code&gt;pg_dump&lt;/code&gt; only needs &lt;code&gt;SELECT&lt;/code&gt;. If you ever want to cut the service off, &lt;code&gt;DROP ROLE dumpling&lt;/code&gt; is enough, and the worst outcome is a failed-backup email.&lt;/p&gt;

&lt;p&gt;For the connection details, open Project Settings, then Database, then Connect, and choose the &lt;strong&gt;session pooler&lt;/strong&gt;: host &lt;code&gt;aws-0-&amp;lt;region&amp;gt;.pooler.supabase.com&lt;/code&gt;, port 5432, user &lt;code&gt;postgres.&amp;lt;project-ref&amp;gt;&lt;/code&gt;. Avoid the direct &lt;code&gt;db.&amp;lt;ref&amp;gt;.supabase.co&lt;/code&gt; host, which is IPv6-only unless you pay for the IPv4 add-on, and avoid the transaction pooler on port 6543, which cannot run &lt;code&gt;pg_dump&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: create a bucket you own
&lt;/h2&gt;

&lt;p&gt;Any S3-compatible bucket works: S3, Backblaze B2, Wasabi, DigitalOcean Spaces, Hetzner, MinIO. For a first bucket I recommend Cloudflare R2, because the first 10 GB per month are free and there are no egress fees, so downloading a backup to restore it costs nothing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the Cloudflare dashboard, open &lt;strong&gt;R2 Object Storage&lt;/strong&gt; and click &lt;strong&gt;Create bucket&lt;/strong&gt;. Name it something like &lt;code&gt;db-backups&lt;/code&gt; and leave the location on Automatic.&lt;/li&gt;
&lt;li&gt;Open &lt;strong&gt;Manage R2 API Tokens&lt;/strong&gt; and click &lt;strong&gt;Create API Token&lt;/strong&gt;. Set the permission to Object Read &amp;amp; Write and restrict it to the &lt;code&gt;db-backups&lt;/code&gt; bucket only. Copy the Access Key ID, the Secret Access Key, and the endpoint, which looks like &lt;code&gt;https://&amp;lt;account-id&amp;gt;.r2.cloudflarestorage.com&lt;/code&gt;. The secret is shown once.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Restricting the token to one bucket matters. If the key ever leaks, only that bucket is exposed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: schedule the backup
&lt;/h2&gt;

&lt;p&gt;In Dumpling, add the database first: host, port, database name, and the read-only user. The connection is tested before it saves. Then add the storage: endpoint, bucket, and the two keys. It writes and then deletes a small marker object to confirm the token works. Finally, create a job that links the two and choose a schedule. The free plan gives you one job with daily runs.&lt;/p&gt;

&lt;p&gt;If you set a passphrase on the job, every file is encrypted with &lt;code&gt;openssl enc -aes-256-cbc -pbkdf2&lt;/code&gt; before it leaves the worker, so you can decrypt it with standard &lt;code&gt;openssl&lt;/code&gt; and nothing else.&lt;/p&gt;

&lt;p&gt;If your database has an IP allow-list, there is one address to add, and it is listed on the docs page.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Run now&lt;/strong&gt; and watch the log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a successful exit code is not enough
&lt;/h2&gt;

&lt;p&gt;Most backup cron jobs, and a fair number of backup products, report success when the dump command exited with 0 and the upload finished. A dump can exit 0 and still be unusable: a custom-format archive with a missing table of contents, a &lt;code&gt;mysqldump&lt;/code&gt; that lost its connection midway and wrote a truncated file, or an empty dump because the user could not see any tables.&lt;/p&gt;

&lt;p&gt;Dumpling verifies every run before it counts as a backup. For Postgres, the dump is passed through &lt;code&gt;pg_restore --list&lt;/code&gt; while it is being uploaded. If &lt;code&gt;pg_restore&lt;/code&gt; cannot read the archive's table of contents, the run is recorded as a failure. For MySQL, the stream is checked for the &lt;code&gt;-- Dump completed&lt;/code&gt; trailer that &lt;code&gt;mysqldump&lt;/code&gt; writes last. The run log shows the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verified: 142 tables with data, 96 indexes, 611 entries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line is the difference between "a file exists in the bucket" and "this file restores".&lt;/p&gt;

&lt;p&gt;The restore command is shown next to every run, so you are not searching for the right flags during an incident:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;createdb restored_db
pg_restore &lt;span class="nt"&gt;--no-owner&lt;/span&gt; &lt;span class="nt"&gt;--no-acl&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; restored_db 2026-09-17T02-18-00Z.dump
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Run a restore drill
&lt;/h2&gt;

&lt;p&gt;A backup is only proven when you restore it. I did this with my own service this week. I took the off-site copy of Dumpling's own database, brought the app up from it on a laptop, and checked that the stored secrets decrypted with the key from my password manager.&lt;/p&gt;

&lt;p&gt;It took ten minutes and thirteen seconds from file to running server. The drill also found a bug in my restore procedure: a Docker volume directory was owned by root after the copy, so SQLite could not write to it. The code was fine, the written procedure was not. That is exactly what drills are for, and I would suggest running one on your own project before you need it.&lt;/p&gt;

&lt;p&gt;For a Supabase project, the drill is the two commands above against a local Postgres, a row count on your largest table, and &lt;code&gt;dropdb restored_db&lt;/code&gt;. It takes about fifteen minutes, and once a quarter is plenty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://getdumpling.dev/backup/supabase-postgres-to-r2" rel="noopener noreferrer"&gt;Supabase to R2 guide&lt;/a&gt;, which covers the same steps with the exact clicks.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://getdumpling.dev/docs#r2" rel="noopener noreferrer"&gt;R2 walkthrough&lt;/a&gt; in the docs.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://getdumpling.dev/security" rel="noopener noreferrer"&gt;security page&lt;/a&gt;, which lists what is stored, what is never stored, and how credentials are protected. Dumpling is run by one person on one server, and the page says so. What makes that acceptable is that your data never passes through storage I control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you already have a cron job with alerting and a restore check, keep it. If you have the cron job without those, this is the fifteen minutes that gets you there.&lt;/p&gt;

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