Nhost Storage lets your users upload and download files. Nhost Storage is partially integrated with the GraphQL API, where file metadata and permissions are managed. Files are stored in S3 and served via a CDN.
Files
Files can be of any type, such as images, documents, videos, etc.
File metadata is stored in your database in the files table in the storage schema. This means that file metadata is available in your GraphQL API, which makes it easy to:
- Read file metadata via GraphQL.
- Manage file permissions (in Hasura).
- Create GraphQL relationships between files and your database tables.
Don’t modify the database schema, nor GraphQL root fields in any of the tables in the storage
schema.
You’re allowed to add and modify the following:
- GraphQL Relationships
- Permissions
Upload File
When a file is uploaded, the file metadata is inserted into the storage.files table and a file id is returned. The file’s id is how you reference and access the file.
You can upload files to a specific bucket and include custom metadata:
// Basic file upload
const uploadResp = await nhost.storage.uploadFiles({
'file[]': [new File(['test content'], 'test-file.txt', { type: 'text/plain' })]
})
// Upload to specific bucket with metadata
const uploadWithMetadata = await nhost.storage.uploadFiles({
'bucket-id': 'user-uploads',
'file[]': [new File(['test content'], 'test-file.txt', { type: 'text/plain' })],
'metadata[]': [{
name: 'custom-filename.txt',
metadata: {
alt: 'Custom description',
category: 'document'
}
}]
})
Download File
There are two ways to download a file:
- Public URL
- Pre-signed URL
Public URL
Public URLs are available for both unauthenticated and authenticated users. Permissions are checked for every file request. Files are served via CDN for optimal performance.
// Download file content
const downloadResp = await nhost.storage.getFile(fileId)
const fileBlob = downloadResp.body
const fileContent = await fileBlob.text()
// Get file metadata headers only (without downloading content)
const metadataResp = await nhost.storage.getFileMetadataHeaders(fileId)
Public URLs support:
- Conditional requests using
If-Modified-Since, If-None-Match headers for caching
- Range requests for partial file downloads using the
Range header
- Image transformations via query parameters (see Image Transformation section)
Pre-signed URL
Pre-signed URLs work differently from public URLs and are ideal for temporary, secure access.
How they work:
- Permission check happens only when requesting the pre-signed URL
- Once generated, anyone with the URL can download the file (no additional auth required)
- URLs expire after a configurable time period
Expiration settings:
- Default bucket: 30 seconds expiration
- Configurable per bucket via the
download_expiration field (in seconds)
// Get pre-signed URL
const resp = await nhost.storage.getFilePresignedURL(fileId)
console.log('URL:', resp.body.url)
console.log('Expires in:', resp.body.expiration, 'seconds')
// Use the URL directly (no auth headers needed)
const fileResponse = await fetch(resp.body.url)
const fileBlob = await fileResponse.blob()
Delete File
Delete a file and the file metadata in the database. This permanently removes both the file content from storage and its associated metadata.
await nhost.storage.deleteFile(fileId)
File deletion is permanent and cannot be undone. Always delete files via the Nhost Storage API to ensure both the file content and metadata are properly removed.
Replace File
Replace an existing file with new content while preserving the file ID. This is useful when you need to update a file without changing references to it.
const newFile = new File(['updated content'], 'updated-file.txt', { type: 'text/plain' })
const replaceResp = await nhost.storage.replaceFile(fileId, {
file: newFile,
metadata: {
name: 'new-filename.txt',
metadata: {
version: '2.0',
updated: new Date().toISOString()
}
}
})
The replace operation follows these steps:
- Sets
isUploaded flag to false during the update process
- Replaces the file content in storage
- Updates file metadata (size, mime-type, etc.)
- Sets
isUploaded flag back to true
Buckets
Buckets are containers used to organize files and group permissions. They provide a way to apply different configurations and access controls to different types of files. Buckets are stored in the storage.buckets table in your database and are accessible via the buckets field in your GraphQL API.
Bucket Configuration
Each bucket can be configured with the following properties:
File Restrictions
- Minimum size - Minimum file size in bytes
- Maximum size - Maximum file size in bytes to prevent large uploads
Access Control
- Cache control - HTTP cache headers for files in this bucket
- Allow pre-signed URLs - Whether files can be accessed via pre-signed URLs
- Download expiration - Expiration time in seconds for pre-signed URLs
Default Bucket
- Every Nhost project includes a
default bucket
- Used automatically when no bucket is specified during upload
- Cannot be deleted (system requirement)
- Has standard configuration suitable for most use cases
Working with Buckets
Upload to Specific Bucket
// Upload to a custom bucket
const uploadResp = await nhost.storage.uploadFiles({
'bucket-id': 'user-profiles',
'file[]': [profileImage]
})
GraphQL Bucket Management
You can query and manage buckets through your GraphQL API:
query GetBuckets {
buckets {
id
minUploadFileSize
maxUploadFileSize
presignedUrlsEnabled
downloadExpiration
cacheControl
}
}
Common Bucket Patterns
User Uploads: Separate public and private user content
- user-public (images, documents accessible to others)
- user-private (personal files, drafts)
Content Types: Organize by file type or purpose
- images (profile pictures, thumbnails)
- documents (PDFs, spreadsheets)
- media (videos, audio files)
Environment-based: Different buckets for different environments
- dev-uploads
- staging-uploads
- prod-uploads
Permissions
Permissions to upload, download, and delete files are managed through Hasura’s permission system on the storage.files table.
Upload
To upload a file, a user must have the insert permission to the storage.files table. The id column must be granted.
The following columns can be used for insert permissions:
id
bucket_id
name
size
mime_type
Download
To download a file, a user must have the select permission to the storage.files table. All columns must be granted.
Delete
To delete a file, a user must have the delete permission to the storage.files table.
Just deleting the file metadata in the storage.files table does not delete the actual file. Always delete files via Nhost Storage. This way, both the file metadata and the actual file are deleted.
Images can be transformed on-the-fly by adding query parameters to file URLs. This feature works with both public URLs and pre-signed URLs, allowing you to resize, optimize, and convert images without pre-processing.
Resize Parameters
w - Maximum width to resize image to while maintaining aspect ratio
h - Maximum height to resize image to while maintaining aspect ratio
q - Image quality (1-100). Applies to JPEG, WebP, and PNG files
f - Output format. Options: auto, same, jpeg, webp, png, avif
auto - Uses content negotiation based on Accept header
same - Keeps original format (default)
Effects
b - Blur the image using sigma value (0 or higher)
Examples
const transformedFile = await nhost.storage.getFile(fileId, {
w: 400,
h: 300,
q: 90,
f: 'webp'
})
Use Cases
Responsive Images: Generate different sizes for various screen resolutions
const thumbnailUrl = `${baseUrl}?w=150&h=150&q=80`
const mobileUrl = `${baseUrl}?w=400&q=85&f=webp`
const desktopUrl = `${baseUrl}?w=1200&q=90&f=webp`
Performance Optimization: Reduce file sizes and improve loading times
const optimizedUrl = `${baseUrl}?q=75&f=webp`
Image Effects: Add visual effects like blur for backgrounds
const blurredBg = `${baseUrl}?w=1920&h=1080&b=10&q=60`
Image transformations are cached at the CDN level for optimal performance. The first request might be slower as the transformed image is generated and cached.