To create a public URL for a Supabase Storage file, use getPublicUrl() on a public bucket or createSignedUrl() for private buckets. Public buckets serve files at a predictable URL without authentication. Private buckets require signed URLs with an expiration time. Choose the right approach based on whether your files should be accessible to anyone or only to authorized users.
Generating Public and Signed URLs for Supabase Storage Files
Supabase Storage offers two ways to create URLs for files: permanent public URLs for files in public buckets and temporary signed URLs for files in private buckets. This tutorial covers both methods, explains when to use each, and shows you how to display files in your frontend application.
Prerequisites
- A Supabase project with a storage bucket containing at least one file
- The Supabase JS client installed (@supabase/supabase-js v2+)
- Understanding of public vs private bucket concepts
- A frontend application (React, Vue, or plain HTML) to display the files
Step-by-step guide
Create a public bucket and upload a test file
Create a public bucket and upload a test file
In the Supabase Dashboard, go to Storage and click New Bucket. Name it 'public-assets' and toggle the Public bucket switch to on. Upload a test image or file using the Dashboard upload button. You can also create the bucket programmatically with the JS client. Public buckets allow anyone to read files using a direct URL without authentication — no RLS policy needed for reads.
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6)78// Create a public bucket (if it doesn't exist)9const { data, error } = await supabase.storage.createBucket('public-assets', {10 public: true,11})Expected result: A public bucket named 'public-assets' is created and a test file is uploaded.
Generate a public URL with getPublicUrl()
Generate a public URL with getPublicUrl()
For files in public buckets, use the getPublicUrl() method. This returns a permanent, predictable URL that does not require authentication. The URL follows the format: https://<project-ref>.supabase.co/storage/v1/object/public/<bucket>/<path>. This method is synchronous and does not make a network request — it constructs the URL from the project reference and file path. The file does not need to exist for getPublicUrl() to return a URL.
1const { data } = supabase.storage2 .from('public-assets')3 .getPublicUrl('images/hero.png')45console.log(data.publicUrl)6// https://<project-ref>.supabase.co/storage/v1/object/public/public-assets/images/hero.pngExpected result: A permanent public URL is generated that anyone can access without authentication.
Generate a signed URL for private bucket files
Generate a signed URL for private bucket files
For files in private buckets, use createSignedUrl() which generates a temporary URL with an expiration time. This is an async operation that calls the Supabase API and returns a URL with an authentication token appended. The second parameter is the expiration time in seconds. Signed URLs are ideal for files that should only be accessible temporarily, like download links in emails or time-limited document sharing.
1// Create a signed URL valid for 1 hour (3600 seconds)2const { data, error } = await supabase.storage3 .from('private-documents')4 .createSignedUrl('invoices/inv-001.pdf', 3600)56if (error) {7 console.error('Failed to create signed URL:', error.message)8} else {9 console.log(data.signedUrl)10}1112// Create multiple signed URLs at once13const { data: urls, error: batchError } = await supabase.storage14 .from('private-documents')15 .createSignedUrls(['file1.pdf', 'file2.pdf'], 3600)Expected result: A temporary signed URL is generated that expires after the specified number of seconds.
Use image transformations with storage URLs
Use image transformations with storage URLs
Supabase Storage supports on-the-fly image transformations for both public and signed URLs. You can resize, crop, and adjust quality by passing a transform option. This is useful for generating thumbnails without storing multiple versions of the same image. Transformations are applied at the CDN level and cached for performance.
1// Public URL with image transformation2const { data } = supabase.storage3 .from('public-assets')4 .getPublicUrl('photos/landscape.jpg', {5 transform: {6 width: 300,7 height: 200,8 resize: 'cover',9 quality: 80,10 },11 })1213// Signed URL with transformation14const { data: signed } = await supabase.storage15 .from('private-photos')16 .createSignedUrl('photos/portrait.jpg', 3600, {17 transform: {18 width: 150,19 height: 150,20 resize: 'cover',21 },22 })Expected result: URLs include transformation parameters and serve resized images at the specified dimensions.
Display storage files in your frontend
Display storage files in your frontend
Use the generated URLs in your frontend to display images, create download links, or embed documents. For React, set the URL as the src attribute on an img tag or the href on an anchor tag. Always handle loading and error states. For signed URLs that expire, consider refreshing them periodically or generating new ones when the user interacts with the file.
1// React component example2function FileDisplay({ bucketName, filePath }: { bucketName: string; filePath: string }) {3 const { data } = supabase.storage4 .from(bucketName)5 .getPublicUrl(filePath)67 return (8 <img9 src={data.publicUrl}10 alt="Stored file"11 onError={(e) => {12 e.currentTarget.src = '/fallback-image.png'13 }}14 />15 )16}Expected result: Files from Supabase Storage are displayed in your frontend application using the generated URLs.
Complete working example
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6)78// === Public bucket URLs ===910// Get a permanent public URL (no auth required to access)11function getPublicFileUrl(bucket: string, path: string): string {12 const { data } = supabase.storage.from(bucket).getPublicUrl(path)13 return data.publicUrl14}1516// Get a public URL with image transformation17function getPublicThumbnail(bucket: string, path: string): string {18 const { data } = supabase.storage.from(bucket).getPublicUrl(path, {19 transform: { width: 200, height: 200, resize: 'cover', quality: 75 },20 })21 return data.publicUrl22}2324// === Private bucket URLs ===2526// Get a temporary signed URL (expires after specified seconds)27async function getSignedFileUrl(28 bucket: string,29 path: string,30 expiresIn: number = 360031): Promise<string | null> {32 const { data, error } = await supabase.storage33 .from(bucket)34 .createSignedUrl(path, expiresIn)3536 if (error) {37 console.error('Signed URL error:', error.message)38 return null39 }40 return data.signedUrl41}4243// Get multiple signed URLs in one call44async function getSignedUrls(45 bucket: string,46 paths: string[],47 expiresIn: number = 360048): Promise<{ path: string; signedUrl: string }[] | null> {49 const { data, error } = await supabase.storage50 .from(bucket)51 .createSignedUrls(paths, expiresIn)5253 if (error) {54 console.error('Batch signed URL error:', error.message)55 return null56 }57 return data58}5960// === Usage examples ===6162const publicUrl = getPublicFileUrl('public-assets', 'logos/logo.svg')63console.log('Public URL:', publicUrl)6465const thumbnail = getPublicThumbnail('public-assets', 'photos/hero.jpg')66console.log('Thumbnail URL:', thumbnail)6768const signedUrl = await getSignedFileUrl('private-docs', 'invoices/inv-001.pdf')69console.log('Signed URL:', signedUrl)Common mistakes when creating a Public URL for a Supabase Storage File
Why it's a problem: Using getPublicUrl() on a private bucket and wondering why the URL returns 400 or empty content
How to avoid: getPublicUrl() only works for public buckets. For private buckets, use createSignedUrl() which appends an authentication token to the URL.
Why it's a problem: Caching signed URLs without accounting for their expiration time
How to avoid: Always check the expiration before using a cached signed URL. Regenerate the URL when it expires. Set cache TTL shorter than the signed URL expiration.
Why it's a problem: Assuming getPublicUrl() verifies the file exists
How to avoid: getPublicUrl() is synchronous and constructs the URL without checking the file. Always handle 404 errors in your frontend with fallback images or error messages.
Best practices
- Use public buckets only for truly public assets like logos and marketing images
- Set signed URL expiration to the minimum time needed for the use case
- Use createSignedUrls() (plural) to batch multiple signed URL requests for performance
- Add image transformations at the URL level instead of storing multiple image sizes
- Always handle image loading errors in the frontend with fallback content
- Store the bucket name and file path in your database, not the full URL, so URLs can be regenerated
- Use the SUPABASE_ANON_KEY for client-side access and never the service role key
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have files in both a public and a private Supabase Storage bucket. Show me how to generate permanent public URLs for the public bucket and temporary signed URLs for the private bucket using the @supabase/supabase-js v2 client. Include image transformation examples.
Create a utility module with functions to generate public URLs, signed URLs, and thumbnail URLs from Supabase Storage. Include a React component that displays an image from a public bucket with a fallback for missing files.
Frequently asked questions
What is the URL format for public Supabase Storage files?
Public file URLs follow this format: https://<project-ref>.supabase.co/storage/v1/object/public/<bucket-name>/<file-path>. The getPublicUrl() method constructs this URL for you.
How long can a signed URL last?
Signed URLs can be set to expire after any number of seconds you specify. There is no maximum enforced by Supabase, but shorter expiration times are more secure. Common values are 300 seconds (5 minutes) for downloads and 3600 seconds (1 hour) for displayed images.
Can I create a permanent URL for a private bucket file?
No, private bucket files can only be accessed through signed URLs which always have an expiration time. If you need permanent public access, move the file to a public bucket or change the bucket's public setting.
Do image transformations cost extra?
Image transformations are available on Pro plans and above at no additional per-transformation cost. They are processed at the CDN edge and cached, so subsequent requests for the same transformation are served from cache.
Can I use getPublicUrl() without initializing the Supabase client?
Technically you can construct the URL manually since the format is predictable, but using the client's getPublicUrl() method is recommended because it handles the URL construction correctly and accounts for custom domains.
Why does my public URL return a 400 Bad Request error?
This usually means the bucket is not set to public. Check the bucket's public setting in the Dashboard under Storage. If the bucket is private, use createSignedUrl() instead.
Can RapidDev help set up a Supabase Storage solution for my application?
Yes, RapidDev can design and implement a complete file storage architecture with Supabase, including bucket configuration, RLS policies, signed URL generation, and image transformation pipelines.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation