Display Images
storage images display authenticated img tag blob URL React guideWhen files are protected by permissions, you can’t just point an <img> tag at the storage URL — the browser won’t send the authentication header. This guide shows how to fetch an image with an authenticated session and display it.
The Problem
Section titled “The Problem”A plain <img src> tag makes an unauthenticated GET request. If your file requires authentication, the request will be rejected:
<!-- This won't work for authenticated files --><img src="https://local.storage.local.nhost.run/v1/files/FILE_ID" />There are three ways to solve this:
- Blob URL (recommended) — fetch the file with the SDK, create a local object URL. CDN-friendly: the CDN caches the file and re-validates with the client’s auth headers.
- Pre-signed URL — generate a temporary public URL. Not CDN-friendly: each URL is unique, bypassing the cache.
- Public bucket — if the file doesn’t need protection, make it public.
Option 1: Blob URL (Recommended for Private Files)
Section titled “Option 1: Blob URL (Recommended for Private Files)”Fetch the image using the SDK (which includes the auth token), then create a blob URL for the <img> tag.
const { body } = await nhost.storage.getFile(fileId, { w: 100, h: 100, f: 'webp',})const url = URL.createObjectURL(body)Thumbnails can be loaded for each image file and stored in a state map:
const loadThumbnail = useCallback( async (fileId: string): Promise<void> => { try { const { body } = await nhost.storage.getFile(fileId, { w: 100, h: 100, f: 'webp', }) const url = URL.createObjectURL(body) setThumbnails((prev) => ({ ...prev, [fileId]: url })) } catch { // Silently skip thumbnails that fail to load } }, [nhost.storage],)Then rendered inline:
{file.mimeType?.startsWith('image/') && file.id && thumbnails[file.id] && ( <img src={thumbnails[file.id]} alt={file.name || ''} style={{ width: 32, height: 32, objectFit: 'cover', borderRadius: 4 }} />)}This is the recommended approach for private files. The request goes through the CDN, which caches the file content. On subsequent requests the CDN re-validates using a conditional request that includes the client’s Authorization header, so the backend only confirms the user still has access without re-serving the file. This gives you both privacy and CDN performance.
Option 2: Pre-signed URL
Section titled “Option 2: Pre-signed URL”Generate a temporary URL that doesn’t require authentication. The image loads like a normal <img> tag.
const { body } = await nhost.storage.getFilePresignedURL(fileId)const url = body.url
// Use as an <img> src — no auth headers neededconst img = document.createElement('img')img.src = urlYou can also store the pre-signed URL in state and display it in a text field for sharing:
const handleGetPresignedUrl = async (fileId: string): Promise<void> => { try { const { body } = await nhost.storage.getFilePresignedURL(fileId) setPresignedUrl(body.url) } catch (err) { const error = err as FetchError<ErrorResponse> setStatusMessage({ message: `Failed to get pre-signed URL: ${error.message}`, isError: true, }) }}Pre-signed URLs are simpler but expire after the bucket’s download_expiration (default: 30 seconds). Each pre-signed URL is unique, which means the CDN treats every generated URL as a different resource — effectively bypassing the cache entirely. Only use pre-signed URLs when you need to share files with systems that cannot send authentication headers. See Pre-signed URLs for details.
Option 3: Public Bucket
Section titled “Option 3: Public Bucket”If the file doesn’t need protection, place it in a bucket with public role select permissions. Then the URL works directly:
<img src="https://local.storage.local.nhost.run/v1/files/FILE_ID?w=400&f=webp" alt="Public image"/>This is the simplest approach and works best with CDN caching. See Permissions for how to configure public access.
Which Approach to Use
Section titled “Which Approach to Use”| Approach | Auth required | CDN-friendly | Expiration | Best for |
|---|---|---|---|---|
| Blob URL (recommended) | Yes | Yes (re-validates with auth headers) | None | Private files in an authenticated UI |
| Pre-signed URL | Only to generate | No (unique URLs bypass cache) | Configurable | Sharing with external systems |
| Public bucket | No | Yes | None | Content accessible to everyone |
- Use blob URLs for private files displayed in an authenticated UI (avatars, user uploads) — this is the preferred approach as it combines privacy with CDN caching
- Use pre-signed URLs only when sharing files with systems that cannot send authentication headers (email links, third-party integrations)
- Use public buckets for content that anyone should be able to see (marketing images, public assets)