Uploading files
Upload files to Supabase Storage on SupaHost from the browser, a server or an Edge Function. Covers upload options, overwriting, signed upload URLs and the 50 MB limit.
Uploads go to your project's Storage API with supabase.storage.from(bucket).upload(). The request carries the user's access token, so RLS on storage.objects decides whether the upload is allowed. Each file can be up to 50 MB on SupaHost.
A basic upload from the browser
This example lets a user attach a PDF to an invoice in an invoicing app. The file goes into a private invoices bucket under the user's own folder.
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!)
async function attachInvoicePdf(invoiceId: number, file: File) {
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error('Sign in first')
const path = `${user.id}/${invoiceId}-${crypto.randomUUID()}.pdf`
const { data, error } = await supabase.storage
.from('invoices')
.upload(path, file, {
contentType: 'application/pdf',
cacheControl: '3600', // seconds browsers may cache the file
upsert: false, // fail instead of overwriting an existing file
})
if (error) throw error
return data.path // store this in your invoices table
}
For this to work you need a bucket called invoices and an insert policy that lets users write to their own folder. See Storage access control.
Upload options
| Option | What it does |
|---|---|
contentType |
The MIME type stored with the file and sent back as Content-Type when it's served. Also checked against the bucket's allowed types. Set it explicitly when uploading a Blob or bytes. |
cacheControl |
The max-age in seconds sent as cache-control when the file is downloaded. Files are served from your instance with no CDN in front, so this is your main caching lever. See Serving and downloading files. |
upsert |
true overwrites an existing file at the same path. false (the default) returns an error if the path is taken. |
What you pass as the body can be a File, Blob, ArrayBuffer, FormData, a typed array, or in Node.js a Buffer or stream.
Overwriting files
Overwriting needs more permissions than a fresh upload. With upsert: true, the user needs insert, select and update policies on the object. To replace a file explicitly, use update():
await supabase.storage
.from('avatars')
.update(`${user.id}/avatar.webp`, newFile, { contentType: 'image/webp', upsert: true })
Overwriting a file at the same path makes caching harder, because browsers may keep showing the old version until cacheControl runs out. For anything public, prefer a new path per version (for example by adding a UUID or timestamp) and save the new path in your table.
Uploading from a server
Server code can upload with the user's token (so RLS applies) or with the service_role key (which bypasses RLS). Use service_role only on the server, and only for files your backend creates itself, like generated reports.
// Node.js: save a generated monthly statement
import { readFile } from 'node:fs/promises'
import { createClient } from '@supabase/supabase-js'
const admin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{ auth: { persistSession: false } }
)
const pdf = await readFile('/tmp/statement-2026-10.pdf')
const { error } = await admin.storage
.from('statements')
.upload('team-7/2026-10.pdf', pdf, { contentType: 'application/pdf', upsert: true })
if (error) throw error
The same code works in an Edge Function with Deno.env.get('SUPABASE_URL') and Deno.env.get('SUPABASE_SERVICE_ROLE_KEY'), which SupaHost injects into every function. See Environment variables and secrets.
Signed upload URLs
Sometimes you want your server to decide where a file goes, but let the browser send the bytes directly. A signed upload URL does that: the server creates a one-time token for one path, and the client uploads to it without needing its own insert policy.
// On the server (service_role)
const { data, error } = await admin.storage
.from('invoices')
.createSignedUploadUrl(`team-7/${invoiceId}.pdf`)
// send data.path and data.token to the browser
// In the browser (anon key)
const { error } = await supabase.storage
.from('invoices')
.uploadToSignedUrl(path, token, file, { contentType: 'application/pdf' })
The token only works for the path it was created for, and it expires. The 50 MB limit still applies.
Moving, copying and removing
const bucket = supabase.storage.from('invoices')
await bucket.move('team-7/draft.pdf', 'team-7/final.pdf')
await bucket.copy('team-7/final.pdf', 'archive/team-7/final.pdf')
await bucket.remove(['team-7/old-1.pdf', 'team-7/old-2.pdf'])
move needs an update policy, copy needs select on the source and insert on the destination, and remove needs delete (plus select). Always remove files through the Storage API. Deleting rows from storage.objects with SQL doesn't remove the files from disk, and they would keep using your file storage quota.
Large files and resumable uploads
On SupaHost a single file can be at most 50 MB. Larger uploads are rejected with a "maximum allowed size" error, whatever the bucket's own limit says. If you need to store bigger files, split them into parts in your app, or compress them before upload.
Standard uploads as shown above are the supported way to upload. Upstream Supabase Storage also has a resumable upload protocol (TUS), which Supabase Cloud recommends for files over 6 MB. SupaHost's documentation doesn't cover it, so build on standard uploads. For files up to 50 MB on a normal connection they work well; if an upload fails, retry it with the same path.
Common errors
| Error | Usual cause |
|---|---|
new row violates row-level security policy (403) |
No insert policy matches the bucket and path. See Storage 403 on upload |
The resource already exists (409) |
A file is already at that path and upsert is false |
The object exceeded the maximum allowed size (413) |
The file is over 50 MB or over the bucket's limit. See Upload file size limits |
mime type ... is not supported |
The contentType isn't in the bucket's allowed types |
For abandoned or half-finished uploads, see Cleaning up failed or partial uploads.