Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Create a Public URL for a Supabase Storage File

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.

What you'll learn

  • How to generate public URLs with getPublicUrl() for public buckets
  • How to create time-limited signed URLs with createSignedUrl() for private buckets
  • The difference between public and private bucket URL access patterns
  • How to use image transformations when generating storage URLs
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate7 min read10-15 minSupabase (all plans), @supabase/supabase-js v2+March 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
1import { createClient } from '@supabase/supabase-js'
2
3const supabase = createClient(
4 process.env.NEXT_PUBLIC_SUPABASE_URL!,
5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6)
7
8// 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.

2

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.

typescript
1const { data } = supabase.storage
2 .from('public-assets')
3 .getPublicUrl('images/hero.png')
4
5console.log(data.publicUrl)
6// https://<project-ref>.supabase.co/storage/v1/object/public/public-assets/images/hero.png

Expected result: A permanent public URL is generated that anyone can access without authentication.

3

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.

typescript
1// Create a signed URL valid for 1 hour (3600 seconds)
2const { data, error } = await supabase.storage
3 .from('private-documents')
4 .createSignedUrl('invoices/inv-001.pdf', 3600)
5
6if (error) {
7 console.error('Failed to create signed URL:', error.message)
8} else {
9 console.log(data.signedUrl)
10}
11
12// Create multiple signed URLs at once
13const { data: urls, error: batchError } = await supabase.storage
14 .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.

4

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.

typescript
1// Public URL with image transformation
2const { data } = supabase.storage
3 .from('public-assets')
4 .getPublicUrl('photos/landscape.jpg', {
5 transform: {
6 width: 300,
7 height: 200,
8 resize: 'cover',
9 quality: 80,
10 },
11 })
12
13// Signed URL with transformation
14const { data: signed } = await supabase.storage
15 .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.

5

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.

typescript
1// React component example
2function FileDisplay({ bucketName, filePath }: { bucketName: string; filePath: string }) {
3 const { data } = supabase.storage
4 .from(bucketName)
5 .getPublicUrl(filePath)
6
7 return (
8 <img
9 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

storage-urls.ts
1import { createClient } from '@supabase/supabase-js'
2
3const supabase = createClient(
4 process.env.NEXT_PUBLIC_SUPABASE_URL!,
5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6)
7
8// === Public bucket URLs ===
9
10// 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.publicUrl
14}
15
16// Get a public URL with image transformation
17function 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.publicUrl
22}
23
24// === Private bucket URLs ===
25
26// Get a temporary signed URL (expires after specified seconds)
27async function getSignedFileUrl(
28 bucket: string,
29 path: string,
30 expiresIn: number = 3600
31): Promise<string | null> {
32 const { data, error } = await supabase.storage
33 .from(bucket)
34 .createSignedUrl(path, expiresIn)
35
36 if (error) {
37 console.error('Signed URL error:', error.message)
38 return null
39 }
40 return data.signedUrl
41}
42
43// Get multiple signed URLs in one call
44async function getSignedUrls(
45 bucket: string,
46 paths: string[],
47 expiresIn: number = 3600
48): Promise<{ path: string; signedUrl: string }[] | null> {
49 const { data, error } = await supabase.storage
50 .from(bucket)
51 .createSignedUrls(paths, expiresIn)
52
53 if (error) {
54 console.error('Batch signed URL error:', error.message)
55 return null
56 }
57 return data
58}
59
60// === Usage examples ===
61
62const publicUrl = getPublicFileUrl('public-assets', 'logos/logo.svg')
63console.log('Public URL:', publicUrl)
64
65const thumbnail = getPublicThumbnail('public-assets', 'photos/hero.jpg')
66console.log('Thumbnail URL:', thumbnail)
67
68const 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.

ChatGPT Prompt

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.

Supabase Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.