docs›Guides›Store and serve files (SDK) Store and serve files with the JavaScript SDK
Everyday storage flows with client.storage: bucket management, single and multipart uploads, streaming downloads, HMAC signed URLs, and trash restoration.
8 min read·Level intermediate·Revised on Oct 28, 2026
#Buckets
1. Bucket management
A bucket groups files and decides whether they are public. Creating and deleting buckets requires a service_role key. Listing buckets requires a signed-in user or a service_role key (the anonymous key receives 403 FORBIDDEN).
server/buckets.tstypescript
// Create a public bucket (anyone can read objects without API keys)
await admin.storage.createBucket('avatars', { public: true })
// Create a private bucket (only object owners or service_role can read)
await admin.storage.createBucket('invoices', {
public: false,
fileSizeLimit: 10 * 1024 * 1024, // 10 MiB limit per file
allowedMimeTypes: ['application/pdf'],
})
// List accessible buckets
const { data: buckets, error } = await admin.storage.listBuckets()
A bucket also accepts fileSizeLimit (in bytes) and allowedMimeTypes. An upload that is too large returns 413 PAYLOAD_TOO_LARGE, and a type that is not allowed returns 400 BAD_REQUEST.
#Files
2. Upload, download, list, remove
files.tstypescript
const bucket = aura.storage.from('invoices')
// Upload: a File, Blob, Buffer, or Uint8Array
// Files > 5 MiB are automatically split into multipart parts
const { data, error } = await bucket.upload(`${user.id}/2026-10.pdf`, file, {
contentType: 'application/pdf',
onUploadProgress: (progress) => {
console.log(`Upload progress: ${progress.percentage}%`)
},
})
// → { key, bucket, url, size, content_type, etag }
// Replace an existing file owned by the current user
await bucket.upload(`${user.id}/2026-10.pdf`, newFile, { upsert: true })
// List files under a prefix / folder
const { data: files } = await bucket.list({ prefix: `${user.id}/` })
// Download file as a Blob
const { data: blob, error: downloadError } = await bucket.download(`${user.id}/2026-10.pdf`)
// Stream download without loading entire file into memory
const { data: stream } = await bucket.download(`${user.id}/2026-10.pdf`).asStream()
// Remove a file (moved to trash by default with 30-day retention)
await bucket.remove([`${user.id}/2026-10.pdf`])
// Permanent deletion (bypasses trash and immediately frees disk quota)
await bucket.remove([`${user.id}/2026-10.pdf`], { permanent: true })
Key runtime behaviors:
- Uploading an existing path returns
409 CONFLICT unless upsert: true is provided.
- Anonymous uploads are rejected with
403 FORBIDDEN on both private and public buckets (writing requires an authenticated JWT or service key).
- Path traversal protection: paths containing
.. or illegal characters return 400 INVALID_PATH without throwing unhandled exceptions.
- User isolation: one user cannot overwrite or delete files owned by another user (
owner_id = auth.uid()). Structure paths using user-scoped folders like ${user.id}/....
#Share
3. Share a file (public & signed URLs)
share.tstypescript
// 1. Public bucket: direct permanent URL (served without API keys)
const { data: { publicUrl } } = aura.storage.from('avatars').getPublicUrl('ada.png')
// → https://<slug>.localhost:8080/v1/storage/avatars/ada.png
// Force download as attachment with custom filename
const downloadUrl = aura.storage.from('avatars').getPublicUrl('ada.png', {
download: 'avatar-ada.png',
})
// 2. Private bucket: HMAC signed URL with configurable TTL
const { data, error } = await aura.storage.from('invoices').createSignedUrl(
`${user.id}/2026-10.pdf`,
300, // Valid for 300 seconds (5 minutes)
{ download: true }
)
// data.signedUrl and data.url contain the HMAC signed URL
// 3. Direct browser upload without exposing secret keys
// Server-side (service_role):
const { data: uploadInfo } = await admin.storage.from('invoices').createSignedUploadUrl(
`${user.id}/document.pdf`,
600
)
// Browser client (no API key required):
await aura.storage.uploadToSignedUrl(uploadInfo.signedUrl, file, {
contentType: 'application/pdf',
})
Signed URLs seal the HTTP method (GET or PUT), bucket, key, and expiration timestamp. Once expired or if the object is deleted, requests receive 403 FORBIDDEN.
#Access
4. Who can do what (Access matrix)
| Action | Anonymous key (anon) | Signed-in user (authenticated) | Service role (service_role) |
|---|
| Read file in a public bucket | Yes | Yes | Yes |
| Read file in a private bucket | No (403) | Owner only (owner_id = auth.uid()) | Yes |
| Upload a new file | No (403) | Yes (becomes owner) | Yes |
Replace (upsert) or delete | No (403) | Owner only | Yes |
| List files in a public bucket | Yes | Yes | Yes |
| List files in a private bucket | No (403) | Owner only (RLS filter) | Yes |
| Create, update, or delete buckets | No (403) | No (403) | Yes |
| Mint signed upload URLs | No (403) | No (403) | Yes |
The owner is identified by the JWT subject (auth.uid()). On both public and private buckets, users cannot overwrite or destroy other users' objects.
#Operations
5. Move, copy, metadata & trash
server/organise.tstypescript
// Copy a file (preserves original Content-Type and custom metadata)
await admin.storage.copy('invoices', '2026/draft.pdf', '2026/archive.pdf')
// Move or rename a file
await admin.storage.move('invoices', '2026/draft.pdf', '2026/final.pdf')
// Inspect object metadata (size, MIME type, ETag, custom metadata)
const { data: meta } = await admin.storage.getMetadata('invoices', '2026/final.pdf')
// List items currently in the trash (30-day retention)
const { data: trashItems } = await admin.storage.from('invoices').listTrash()
// Restore an object from trash
await admin.storage.from('invoices').restore('2026/final.pdf')
For real-time image transformations (resizing, format conversion, rotation), see the Storage SDK Reference.
Last updated · Oct 28, 2026