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 withnosniff). - 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 DOMimg.src = data.publicUrlEvery 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.