Aurabase Logo
aurabasedocs
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)

ActionAnonymous key (anon)Signed-in user (authenticated)Service role (service_role)
Read file in a public bucketYesYesYes
Read file in a private bucketNo (403)Owner only (owner_id = auth.uid())Yes
Upload a new fileNo (403)Yes (becomes owner)Yes
Replace (upsert) or deleteNo (403)Owner onlyYes
List files in a public bucketYesYesYes
List files in a private bucketNo (403)Owner only (RLS filter)Yes
Create, update, or delete bucketsNo (403)No (403)Yes
Mint signed upload URLsNo (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