Skip to content

Public URLs

A public URL serves an object without any credential — ideal for avatars, public assets, and anything you’d drop into an <img src>. Public URLs only work for objects in a bucket whose public flag is true (bucket management).

The endpoint

GET /v1/storage/<project_id>/object/public/<bucket>/<path>

This route is served outside authentication — no apikey or JWT required. It is deliberately strict about disclosure:

  • If the bucket is public, the object bytes are returned (with the same content-type XSS hardening as the authenticated download — HTML/SVG/XML are forced to an attachment with nosniff).
  • If the bucket is private, suspended, or doesn’t exist, the response is an opaque 404 — indistinguishable cases, so an anonymous caller can’t probe for the existence of private objects.

From the SDK

getPublicUrl(path) builds the URL with no network call (matching storage-js):

const { data } = db.storage.from('avatars').getPublicUrl('user-123/photo.jpg')
// data.publicUrl →
// https://<host>/v1/storage/<project>/object/public/avatars/user-123/photo.jpg
// drop it straight into the DOM
img.src = data.publicUrl

Every SDK exposes the same builder (get_public_url / getPublicUrl). Because it doesn’t call the server, it returns a URL even for a private bucket — but that URL will 404 until the bucket is made public.

With a transform

Pass a transform to get a render/image URL instead:

const { data } = db.storage.from('avatars').getPublicUrl('user-123/photo.jpg', {
transform: { width: 200, height: 200, resize: 'cover', format: 'webp' },
})

Making a bucket public

await db.storage.updateBucket('avatars', { public: true })

Public means anyone with the URL can read the object. For per-user private objects, keep the bucket private and use signed URLs or owner-prefix RLS instead.

Caching

Public URLs are deterministic and cache-friendly — put a CDN or Traefik cache in front of the control plane to serve them from the edge.