[{"data":1,"prerenderedAt":28},["ShallowReactive",2],{"$fvY-PCecN27FcRz98G3R1BAv5HGaMvIgnmmgv4Q1PYoQ":3},{"slug":4,"title":5,"description":6,"date":7,"tags":8,"draft":12,"readingMinutes":13,"html":14,"related":15},"supabase-storage","Supabase Storage in Production: Policies, URLs and Limits","Supabase Storage checked November 2026: RLS on storage.objects, public vs signed URLs, the 50 MB and 6 MB lines, resumable uploads, and what it bills.","2026-11-08",[9,10,11],"supabase","storage","security",false,4,"\u003Cp>Supabase Storage is an S3-style file store that sits behind the same auth and Postgres policies as the rest of Supabase. Buckets hold the files, but who can upload, read or delete them is decided by row-level security policies on one table, \u003Ccode>storage.objects\u003C/code>. Get those policies and the bucket type right and Storage is boring in the good way. Get them wrong and you either can&#39;t upload anything or everyone can read everything.\u003C/p>\n\u003Cp>I checked everything below on November 8, 2026 against Supabase&#39;s docs on \u003Ca href=\"https://supabase.com/docs/guides/storage/security/access-control\">access control\u003C/a>, \u003Ca href=\"https://supabase.com/docs/guides/storage/uploads/file-limits\">file limits\u003C/a>, \u003Ca href=\"https://supabase.com/docs/guides/storage/uploads/resumable-uploads\">resumable uploads\u003C/a>, \u003Ca href=\"https://supabase.com/docs/guides/storage/serving/downloads\">serving files\u003C/a> and \u003Ca href=\"https://supabase.com/docs/guides/storage/pricing\">Storage pricing\u003C/a>.\u003C/p>\n\u003Ch2>Policies live on storage.objects\u003C/h2>\n\u003Cp>The first surprise for most people: a new bucket with no policies accepts no uploads. Supabase is explicit about it: &quot;By default Storage does not allow any uploads to buckets without RLS policies.&quot; The error you get looks like a permissions bug in your code. It isn&#39;t. It&#39;s a missing policy.\u003C/p>\n\u003Cp>What each operation needs:\u003C/p>\n\u003Cul>\n\u003Cli>Upload needs an \u003Ccode>INSERT\u003C/code> policy.\u003C/li>\n\u003Cli>Overwriting an existing file (upsert) also needs \u003Ccode>SELECT\u003C/code> and \u003Ccode>UPDATE\u003C/code>. This one bites when uploads work on day one and &quot;replace avatar&quot; fails on day two.\u003C/li>\n\u003Cli>Reading and deleting need \u003Ccode>SELECT\u003C/code> and \u003Ccode>DELETE\u003C/code>.\u003C/li>\n\u003C/ul>\n\u003Cp>The pattern I&#39;d use for anything user-owned is a folder per user, checked in the policy with the \u003Ccode>storage.foldername()\u003C/code> helper:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sql\">create policy &quot;users write their own folder&quot;\non storage.objects for insert to authenticated\nwith check (\n  bucket_id = &#39;uploads&#39; and\n  (storage.foldername(name))[1] = (select auth.uid()::text)\n);\n\u003C/code>\u003C/pre>\n\u003Cp>Then the client uploads to \u003Ccode>uploads/&lt;user id&gt;/whatever.pdf\u003C/code>, and nobody can write into someone else&#39;s folder no matter what the front end sends. Copy the same condition into the \u003Ccode>select\u003C/code>, \u003Ccode>update\u003C/code> and \u003Ccode>delete\u003C/code> policies. Supabase&#39;s own examples also use the \u003Ccode>owner_id\u003C/code> column, which Storage fills with the uploader&#39;s id, if you prefer ownership over folders.\u003C/p>\n\u003Cp>The service role key skips all of this. Supabase says it &quot;entirely bypass[es] RLS policies&quot;. That&#39;s what you want in a server job that cleans up old files, and it&#39;s a disaster in a browser bundle. If \u003Ccode>service_role\u003C/code> ever appears in front-end code, rotate it today.\u003C/p>\n\u003Ch2>Public buckets, private buckets, signed URLs\u003C/h2>\n\u003Cp>A public bucket means what it says: anyone with the URL can fetch the file, no token needed, and those files get a high CDN cache hit rate. That&#39;s right for product images, avatars and blog assets. It&#39;s wrong for invoices, ID scans and anything a user would be upset to see on someone else&#39;s screen.\u003C/p>\n\u003Cp>Private buckets are reached two ways: a signed URL from \u003Ccode>createSignedUrl(path, expiresIn)\u003C/code>, or a GET to the \u003Ccode>/object/authenticated/\u003C/code> endpoint with the user&#39;s own token. Signed URLs are convenient for \u003Ccode>&lt;img&gt;\u003C/code> tags and downloads, with one property you should plan around: \u003Cstrong>you can&#39;t revoke a single signed URL.\u003C/strong> The docs give two ways out: delete, move or rename the file, or rotate the project&#39;s URL signing key, which kills every signed URL at once. So keep the expiry short. I&#39;d use minutes for anything sensitive, not the hour most examples show.\u003C/p>\n\u003Cp>Public buckets have their own gotcha. The docs&#39; public-read example adds \u003Ccode>storage.allow_any_operation()\u003C/code> so people can fetch files without being able to list the bucket. If your bucket names are guessable and the files aren&#39;t meant to be browsed, check that listing is actually off.\u003C/p>\n\u003Ch2>The 6 MB line and the 50 MB line\u003C/h2>\n\u003Cimg src=\"/blog/supabase-storage/storage-size-ruler.webp\" alt=\"File-size ruler for Supabase Storage: resumable uploads from 6 MB, a 50 MB ceiling on Free, up to 500 GB on Pro\" width=\"1600\" height=\"900\" loading=\"lazy\">\u003Cp>Two numbers decide how uploads should work.\u003C/p>\n\u003Cp>\u003Cstrong>50 MB\u003C/strong> is the most a Free project&#39;s global file size limit can be set to. Pro lets you raise the global limit up to 500 GB. Each bucket can have its own lower limit and an allowed list of MIME types, and I&#39;d set both on every bucket: a profile picture bucket that accepts 2 GB \u003Ccode>.zip\u003C/code> files is an invitation.\u003C/p>\n\u003Cp>\u003Cstrong>6 MB\u003C/strong> is where Supabase recommends switching from a plain upload to a resumable one over TUS. The resumable endpoint has a few rules that aren&#39;t optional:\u003C/p>\n\u003Cul>\n\u003Cli>\u003Ccode>chunkSize\u003C/code> must be 6 MB. The docs literally say &quot;do not change it&quot;.\u003C/li>\n\u003Cli>Use the direct storage host, \u003Ccode>https://&lt;project-id&gt;.storage.supabase.co/storage/v1/upload/resumable\u003C/code>, for better performance.\u003C/li>\n\u003Cli>Each upload URL lives up to 24 hours, then the upload starts over.\u003C/li>\n\u003Cli>Two clients on the same upload URL: one wins, the others get \u003Ccode>409 Conflict\u003C/code>. Same path, different URLs: the first to finish wins, unless you send \u003Ccode>x-upsert\u003C/code>, and then the last one wins.\u003C/li>\n\u003C/ul>\n\u003Cp>Uploading to a path that already exists returns \u003Ccode>400 Asset Already Exists\u003C/code> unless you upsert, and Supabase advises against overwriting at all, because the CDN can keep serving the old file for a while. I agree with them: write each version to a new path (\u003Ccode>avatar-&lt;timestamp&gt;.webp\u003C/code>) and update the database row that points to it.\u003C/p>\n\u003Cp>File names are picky too. Letters, digits, whitespace and a short list of punctuation are allowed. A user uploading \u003Ccode>résumé (final)#2.pdf\u003C/code> will find out which characters aren&#39;t, so slugify names before upload.\u003C/p>\n\u003Ch2>What it bills\u003C/h2>\n\u003Cp>Storage size is billed by the GB-hour: $0.00002919, which works out to $0.0213 per GB-month. Free includes 1 GB, Pro and Team 100 GB, and you only pay past the quota. Downloads count as egress, and egress is split in two on Pro: 250 GB uncached and 250 GB cached included, then \u003Ca href=\"https://supabase.com/docs/guides/platform/manage-your-usage/egress\">$0.09 and $0.03 per GB\u003C/a>. Image transformations are an extra charge on top.\u003C/p>\n\u003Cp>That split is the cheapest lever. Files served from the CDN cost a third of uncached ones, and public buckets get far more cache hits than signed URLs that change on every request. So a public bucket for public assets isn&#39;t only simpler, it&#39;s cheaper.\u003C/p>\n\u003Cp>A lot of apps built with Lovable use exactly this setup, with buckets created early and policies nobody has read since. If that&#39;s yours, my agency \u003Ca href=\"https://lovable2live.com/\">Lovable 2 Live\u003C/a> reviews and fixes them. For anything else, \u003Ca href=\"https://calendly.com/director-norsky/meeting\">book a free call\u003C/a>. All consultation is free.\u003C/p>\n",[16,20,24],{"slug":17,"title":18,"description":19},"vibe-coding-security","Vibe Coding Security: The Five Things I Check First","Vibe coding security in plain terms: the five checks I run on an AI-built app before real users arrive, from database rules to keys in the bundle.",{"slug":21,"title":22,"description":23},"supabase-rls","Supabase RLS: What It Actually Protects","Supabase row level security explained for 2026: grants vs policies, the publishable key, why UPDATE needs a SELECT policy, views that leak, and fast policies.",{"slug":25,"title":26,"description":27},"supabase-cli","Supabase CLI: Migrations, Types and the Docker Catch","The Supabase CLI in late 2026: how to install it, why local dev needs a container runtime, and the migration commands that keep production in sync.",1794181975305]