Storage JavaScript SDK
The Volcano JavaScript SDK provides a Supabase-like interface for storage operations.
The Volcano JavaScript SDK provides a Supabase-like interface for storage operations. All methods return promises with { data, error } patterns for consistent error handling.
Installation
npm install @volcano.dev/sdk
# or
yarn add @volcano.dev/sdkSetup
import { VolcanoAuth } from '@volcano.dev/sdk';
const volcano = new VolcanoAuth({
apiUrl: 'https://api.yourapp.com',
anonKey: 'your-anon-key'
});
// Sign in to access storage
await volcano.auth.signIn({ email: 'user@example.com', password: 'password' });Selecting a bucket
All storage operations start by selecting a bucket:
const bucket = volcano.storage.from('my-bucket');The returned object provides all file operations for that bucket.
Upload
Upload files to storage. Supports File, Blob, and ArrayBuffer inputs.
Basic upload
// From file input
const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0];
const { data, error } = await volcano.storage
.from('avatars')
.upload('user/avatar.png', file);
if (error) {
console.error('Upload failed:', error.message);
} else {
console.log('Uploaded:', data);
// { id, name, size, mime_type, created_at, ... }
}Upload with custom content type
const blob = new Blob([jsonData], { type: 'application/json' });
const { data, error } = await volcano.storage
.from('data')
.upload('config.json', blob, {
contentType: 'application/json'
});Upload from ArrayBuffer
const buffer = await file.arrayBuffer();
const { data, error } = await volcano.storage
.from('files')
.upload('binary.dat', buffer, {
contentType: 'application/octet-stream'
});Drag and drop upload
const dropZone = document.getElementById('drop-zone');
dropZone.addEventListener('drop', async (e) => {
e.preventDefault();
for (const file of e.dataTransfer.files) {
const { data, error } = await volcano.storage
.from('uploads')
.upload(file.name, file);
if (error) {
console.error(`Failed to upload ${file.name}:`, error.message);
}
}
});Download
Download files as Blob objects.
Basic download
const { data, error } = await volcano.storage
.from('documents')
.download('report.pdf');
if (error) {
console.error('Download failed:', error.message);
return;
}
// data is a Blob
const url = URL.createObjectURL(data);
window.open(url);Download and display image
const { data, error } = await volcano.storage
.from('avatars')
.download('user/profile.jpg');
if (!error) {
const img = document.getElementById('avatar');
img.src = URL.createObjectURL(data);
}Download and save
const { data, error } = await volcano.storage
.from('exports')
.download('data.csv');
if (!error) {
const url = URL.createObjectURL(data);
const link = document.createElement('a');
link.href = url;
link.download = 'data.csv';
link.click();
URL.revokeObjectURL(url);
}Partial download (Range requests)
// Download first 1KB of a file
const { data, error } = await volcano.storage
.from('large-files')
.download('video.mp4', { range: 'bytes=0-1023' });List files
List files in a bucket with optional prefix filtering and pagination.
List all files
const { data: files, error, nextCursor } = await volcano.storage
.from('uploads')
.list();
if (!error) {
files.forEach(file => {
console.log(`${file.name} - ${file.size} bytes - ${file.mime_type}`);
});
}List with prefix (folder)
const { data: files, error } = await volcano.storage
.from('uploads')
.list('user/documents/');
// Returns only files starting with "user/documents/"Pagination
// First page
const { data: files, nextCursor } = await volcano.storage
.from('uploads')
.list('', { limit: 100 });
// Next page
if (nextCursor) {
const { data: moreFiles } = await volcano.storage
.from('uploads')
.list('', { limit: 100, cursor: nextCursor });
}File object properties
Each file in the list includes:
interface StorageObject {
id: string; // Unique identifier
bucket_id: string; // Parent bucket ID
name: string; // Full path/name
owner_id?: string; // User who uploaded the file
size: number; // Size in bytes
mime_type: string; // MIME type
etag?: string; // S3 ETag for caching
metadata?: object; // Custom metadata
created_at: string; // ISO timestamp
updated_at: string; // ISO timestamp
}Delete
Delete one or more files.
Delete single file
const { data, error } = await volcano.storage
.from('uploads')
.remove('old-file.txt');
if (!error) {
console.log('Deleted:', data.deleted);
}Delete multiple files
const { data, error } = await volcano.storage
.from('uploads')
.remove(['file1.txt', 'file2.txt', 'folder/file3.txt']);
if (error) {
// Note: error is set if ANY file failed, but data.deleted still
// contains the list of successfully deleted files
console.error('Some files failed to delete:', error.message);
}
// Always check data.deleted for successfully removed files
console.log('Successfully deleted:', data?.deleted || []);Partial success: When deleting multiple files,
erroris set if any file fails, butdata.deletedstill contains the paths of successfully deleted files. Always check both.
Move / Rename
Move or rename a file within the same bucket.
const { data, error } = await volcano.storage
.from('documents')
.move('drafts/report.docx', 'published/report.docx');
if (!error) {
console.log('Moved to:', data.name);
}Copy
Create a copy of a file.
const { data, error } = await volcano.storage
.from('templates')
.copy('invoice-template.pdf', 'invoices/2024-001.pdf');
if (!error) {
console.log('Copied to:', data.name);
}File visibility
Files are private by default. You can make individual files public so they can be downloaded with just an anon key (no user authentication required).
Make a file public
const { data, error } = await volcano.storage
.from('documents')
.updateVisibility('report.pdf', true);
if (!error) {
console.log('File is now public:', data.is_public);
}Make a file private
const { data, error } = await volcano.storage
.from('documents')
.updateVisibility('report.pdf', false);
if (!error) {
console.log('File is now private');
}Who can change visibility
- File owner: The user who uploaded the file
- Service key: Has admin access to all files
Other users cannot change a file's visibility, even if they can read or write the file.
Public vs private download
// Private file - requires user authentication + policy check
const userToken = 'user-access-token';
volcano.accessToken = userToken;
const { data } = await volcano.storage.from('files').download('private.pdf');
// Public file - only needs anon key (identifies project, no user auth)
volcano.accessToken = anonKey;
const { data: publicData } = await volcano.storage.from('files').download('public.pdf');Public URLs (Shareable Links)
Get a truly shareable URL for public files. The URL requires no authentication - it can be shared anywhere.
// First, make the file public
await volcano.storage
.from('avatars')
.updateVisibility('profile.jpg', true);
// Then get the shareable URL
const { data, error } = volcano.storage
.from('avatars')
.getPublicUrl('profile.jpg');
if (!error) {
console.log(data.publicUrl);
// https://api.yourapp.com/public/{projectId}/avatars/profile.jpg
}URL format
https://api.yourapp.com/public/{projectId}/{bucketName}/{filePath}Usage examples
// In HTML (no SDK needed)
<img src="https://api.yourapp.com/public/proj-123/avatars/profile.jpg" />
// Share via email, embed in external sites, etc.
const shareUrl = data.publicUrl;Key points
- No authentication required - Works in any browser, can be embedded anywhere
- Only for public files - Returns 403 for files with
is_public: false - All requests go through Volcano - No direct S3 access, all security enforced
- Project isolation - Each project has separate namespacing
Error handling
All methods return { data, error }. Always check for errors:
const { data, error } = await volcano.storage
.from('files')
.upload('document.pdf', file);
if (error) {
switch (true) {
case error.message.includes('not found'):
console.error('Bucket does not exist');
break;
case error.message.includes('denied'):
console.error('Access denied by storage policy');
break;
case error.message.includes('size'):
console.error('File too large');
break;
case error.message.includes('MIME'):
console.error('File type not allowed');
break;
default:
console.error('Upload failed:', error.message);
}
return;
}
console.log('Success:', data);TypeScript
The SDK includes full TypeScript definitions:
import { VolcanoAuth, StorageObject, StorageUploadResponse } from '@volcano.dev/sdk';
const volcano = new VolcanoAuth({
apiUrl: 'https://api.yourapp.com',
anonKey: 'your-anon-key'
});
async function uploadFile(file: File): Promise<StorageObject | null> {
const { data, error }: StorageUploadResponse = await volcano.storage
.from('uploads')
.upload(file.name, file);
if (error) {
console.error(error);
return null;
}
return data;
}Resumable uploads (large files)
For files over 100MB or when you need resume capability for unreliable networks, use resumable uploads.
Quick method: uploadResumable()
The simplest way to upload large files with progress tracking:
const { data, error } = await volcano.storage
.from('uploads')
.uploadResumable('large-video.mp4', file, {
onProgress: (uploaded, total) => {
const percent = Math.round(uploaded / total * 100);
console.log(`Uploading: ${percent}%`);
progressBar.style.width = `${percent}%`;
}
});
if (!error) {
console.log('Upload complete:', data.name, data.size);
}Manual control
For full control over the upload process (parallel uploads, custom retry logic):
1. Create upload session
const { data: session, error } = await volcano.storage
.from('uploads')
.createUploadSession('large-video.mp4', {
totalSize: file.size,
contentType: 'video/mp4'
});
if (error) {
console.error('Failed to create session:', error.message);
return;
}
console.log('Session created:', session.session_id);
console.log(`Will upload in ${session.total_parts} parts of ${session.part_size} bytes`);2. Upload parts
Parts can be uploaded in any order and even in parallel:
const { total_parts, part_size, session_id } = session;
for (let partNumber = 1; partNumber <= total_parts; partNumber++) {
const start = (partNumber - 1) * part_size;
const end = Math.min(start + part_size, file.size);
const partData = file.slice(start, end);
const { error } = await volcano.storage
.from('uploads')
.uploadPart('large-video.mp4', session_id, partNumber, partData);
if (error) {
console.error(`Part ${partNumber} failed:`, error.message);
// Can retry this part later
} else {
console.log(`Part ${partNumber}/${total_parts} uploaded`);
}
}3. Complete the upload
const { data, error } = await volcano.storage
.from('uploads')
.completeUploadSession('large-video.mp4', session_id);
if (!error) {
console.log('Upload complete:', data.name, data.size);
}Resume interrupted uploads
If an upload is interrupted, you can check progress and resume:
// Get session status
const { data: status, error } = await volcano.storage
.from('uploads')
.getUploadSession('large-video.mp4', savedSessionId);
if (error) {
console.error('Session not found or expired');
return;
}
console.log(`Progress: ${status.uploaded_parts}/${status.total_parts} parts`);
console.log(`${status.uploaded_bytes} of ${status.total_size} bytes uploaded`);
console.log('Missing parts:', status.missing_parts);
// Upload only the missing parts
for (const partNumber of status.missing_parts) {
const start = (partNumber - 1) * status.part_size;
const end = Math.min(start + status.part_size, file.size);
const partData = file.slice(start, end);
await volcano.storage
.from('uploads')
.uploadPart('large-video.mp4', savedSessionId, partNumber, partData);
}
// Complete the upload
await volcano.storage
.from('uploads')
.completeUploadSession('large-video.mp4', savedSessionId);Abort an upload
To cancel an upload and clean up uploaded parts:
const { error } = await volcano.storage
.from('uploads')
.abortUploadSession('large-video.mp4', sessionId);
if (!error) {
console.log('Upload aborted and cleaned up');
}Resumable upload limits
| Limit | Value |
|---|---|
| Minimum part size | 5MB |
| Maximum part size | 25MB |
| Default part size | 25MB |
| Maximum parts | 10,000 |
| Maximum file size | Limited by plan storage quota (S3 supports up to 5TB) |
| Session expiry | 7 days |
When to use resumable uploads
| Scenario | Recommended method |
|---|---|
| Files under 100MB | upload() - simple, single request |
| Files over 100MB | uploadResumable() - automatic chunking |
| Unreliable network | uploadResumable() with onProgress to save state |
| Need to pause/resume | Manual methods with createUploadSession() |
| Very large files (1GB+) | Manual methods for better control |
Complete example
See the Next.js example for a complete implementation with:
- File upload with drag-and-drop
- File listing with icons
- Download functionality
- Delete with confirmation
- Error handling
// From examples/frontend-auth-nextjs/app/files/page.tsx
const handleUpload = async (files: FileList) => {
for (const file of Array.from(files)) {
const { error } = await volcano.storage
.from('user-files')
.upload(file.name, file);
if (error) {
console.error(`Failed to upload ${file.name}:`, error.message);
}
}
// Refresh file list
loadFiles();
};
const handleDownload = async (file: StorageObject) => {
const { data, error } = await volcano.storage
.from('user-files')
.download(file.name);
if (!error && data) {
const url = URL.createObjectURL(data);
const a = document.createElement('a');
a.href = url;
a.download = file.name;
a.click();
URL.revokeObjectURL(url);
}
};