Storage
Maravilla Storage is an object store for files of any size — think S3-style buckets, without the provisioning step. Supports direct server uploads, presigned URLs for browser-to-storage uploads, download URL generation, and per-object metadata and tags. Every Maravilla project automatically gets one object store — you can have as many keys and prefixes inside it as you want. Files live locally during maravilla dev and in Maravilla Cloud in production; same code, both environments.
Object keys are created on first write. There is no provisioning step — no buckets to create, no prefixes to declare; just put and the path exists. During maravilla dev files persist on disk under the CLI’s data directory; in production they live in Maravilla Cloud. In the example below, the docs/ prefix and the report.pdf key both come into being on the put call.
import { getPlatform } from '@maravilla-labs/platform';
const platform = getPlatform();
await platform.STORAGE.put('docs/report.pdf', pdfBuffer, {
contentType: 'application/pdf'
});
API Reference
put(key, data, metadata?)
Uploads a file from the server. Use this for small files or when the data is already available on the server side.
Parameters:
key(string) — unique file path/keydata(Uint8Array | ArrayBuffer) — the file contentmetadata(object, optional):contentType(string) — MIME typefilename(string) — original filenametags(object) — custom tags
await platform.STORAGE.put('uploads/photo.jpg', imageBuffer, {
contentType: 'image/jpeg',
filename: 'vacation.jpg',
tags: { uploadedBy: 'user-123' }
});
get(key)
Retrieves a file and its metadata.
Returns: { data: Uint8Array, metadata: { ... } }
const file = await platform.STORAGE.get('uploads/photo.jpg');
// file.data -- Uint8Array of the file content
// file.metadata -- associated metadata object
delete(key)
Deletes a file from storage.
await platform.STORAGE.delete('uploads/photo.jpg');
confirm(key)
Belt-and-braces idempotent re-verifier. Most apps never need this — every upload path through Maravilla’s API (direct put(), token upload, presigned URL) publishes a storage.put event on its own the moment the file lands, and onStorage handlers + declarative transforms blocks fire automatically.
confirm() exists for edge cases where a file was placed into the bucket outside the platform’s API (e.g. by a bulk migration script) and you want to retroactively announce it. It HEADs the object to verify it actually exists, then publishes the same storage.put event a regular upload would have produced.
await platform.STORAGE.confirm('imports/backfill/2024-q1.csv');
list(prefix?, limit?)
Lists files in storage, optionally filtered by prefix.
Parameters:
prefix(string, optional, default:'') — only return files whose keys start with this prefixlimit(number, optional, default: 100, max: 1000) — maximum number of results
const files = await platform.STORAGE.list('uploads/', 50);
getMetadata(key)
Returns only the metadata for a file, without downloading the file content. Useful for checking file details without transferring the data.
const metadata = await platform.STORAGE.getMetadata('uploads/photo.jpg');
// { contentType, filename, size, ... }
generateUploadUrl(key, contentType, size)
Generates a presigned URL that allows a client (typically a browser) to upload a file directly to storage, bypassing your server. This is the recommended approach for large files.
Parameters:
key(string) — the storage key the file will be saved undercontentType(string) — the MIME type of the file being uploadedsize(number) — maximum allowed file size in bytes
Returns: { url, method, headers }
const uploadUrl = await platform.STORAGE.generateUploadUrl(
'uploads/avatar.png',
'image/png',
5 * 1024 * 1024 // 5 MB max
);
// Return uploadUrl to the client for direct upload
// uploadUrl.url -- the presigned URL
// uploadUrl.method -- HTTP method to use (typically "PUT")
// uploadUrl.headers -- headers the client must include
generateDownloadUrl(key, expiresInSeconds?)
Generates a temporary presigned URL for downloading a file. Useful for serving private files to authenticated users without exposing your storage credentials.
Parameters:
key(string) — the file keyexpiresInSeconds(number, optional) — seconds until the URL expires (default: 900 / 15 minutes)
Returns: { url, method, headers, expiresIn }
const download = await platform.STORAGE.generateDownloadUrl(
'reports/q1.pdf',
3600 // 1 hour
);
// Redirect user to download.url
Upload Patterns
Server Upload — Recommended
The simplest and most common pattern. The file goes through your server, where you can validate, preview, or process it before storing.
// Server endpoint (e.g., SvelteKit +server.ts)
import { json } from '@sveltejs/kit';
import { getPlatform } from '@maravilla-labs/platform';
export const POST = async ({ request }) => {
const platform = getPlatform();
const formData = await request.formData();
const file = formData.get('file');
const key = `uploads/${Date.now()}-${file.name}`;
// Validate
if (file.size > 10 * 1024 * 1024) {
return json({ error: 'File too large' }, { status: 400 });
}
// Store
await platform.STORAGE.put(key, new Uint8Array(await file.arrayBuffer()), {
contentType: file.type,
filename: file.name
});
return json({ success: true, key });
};
This approach lets you:
- Validate file type and size before storing
- Generate previews or thumbnails
- Transform or resize images
- Apply business logic (permissions, quotas)
Token Upload
For larger files, generate an upload token on the server and let the client upload directly to the platform’s upload endpoint. This avoids loading the full file into your server’s memory.
Step 1: Server generates the upload token
// Server endpoint (e.g., SvelteKit +server.ts)
import { json } from '@sveltejs/kit';
import { getPlatform } from '@maravilla-labs/platform';
export const POST = async ({ request }) => {
const platform = getPlatform();
const { key, content_type, size } = await request.json();
const uploadUrl = await platform.STORAGE.generateUploadUrl(
key,
content_type,
size || 10 * 1024 * 1024
);
return json({
upload_url: uploadUrl.url,
method: uploadUrl.method,
headers: uploadUrl.headers
});
};
Step 2: Client uploads using the token
The returned upload_url points to the platform’s upload endpoint (e.g., /api/storage/upload/{token}). The client uploads to this URL:
async function uploadFile(file) {
const key = `uploads/${Date.now()}-${file.name}`;
// Get upload token from your server
const res = await fetch('/api/uploads/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key,
content_type: file.type,
size: file.size
})
});
const { upload_url, method, headers } = await res.json();
// Upload to the platform's upload endpoint
await fetch(upload_url, {
method: method || 'PUT',
headers: headers || {},
body: file
});
return key;
}
The token is validated server-side (expiration, size limit, content type) before the file is stored. Tokens expire after 15 minutes by default.
Preview Before Storing
When you need users to preview a file (e.g., crop an image, confirm a document) before committing it to storage:
// 1. Client previews locally using a blob URL
const previewUrl = URL.createObjectURL(file);
// Show preview in the UI...
// 2. User confirms — then upload via server
const formData = new FormData();
formData.append('file', file);
await fetch('/api/uploads', { method: 'POST', body: formData });
// 3. Clean up preview
URL.revokeObjectURL(previewUrl);
This keeps the file in the browser until the user is ready, avoiding unnecessary storage writes and cleanup.
Combining Storage with KV for Metadata
Store file metadata in the KV Store for fast lookups without reading the file:
const platform = getPlatform();
// Upload file to storage
const key = `uploads/${crypto.randomUUID()}-${file.name}`;
await platform.STORAGE.put(key, fileData, {
contentType: file.type,
filename: file.name
});
// Store metadata in KV for quick access
await platform.KV.files.put(`metadata:${key}`, JSON.stringify({
filename: file.name,
contentType: file.type,
size: file.size,
uploadedAt: new Date().toISOString()
}));
Security Best Practices
File Validation
Always validate uploads on the server side, even when using presigned URLs:
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 10 * 1024 * 1024; // 10 MB
if (!ALLOWED_TYPES.includes(file.type)) {
throw new Error('Invalid file type');
}
if (file.size > MAX_SIZE) {
throw new Error('File too large');
}
Secure Key Generation
Use unique, collision-resistant keys:
import { randomUUID } from 'crypto';
const key = `uploads/${userId}/${randomUUID()}-${file.name}`;
Presigned URL Expiration
Keep presigned URL lifetimes short:
const uploadUrl = await platform.STORAGE.generateUploadUrl(
key,
contentType,
{
sizeLimit: 10 * 1024 * 1024, // enforce size limit
expires_in: 300 // 5 minutes
}
);
Environment Details
| Setting | Value |
|---|---|
| Max file size (Free) | 10 MB |
| Max file size (Pro) | 50 MB |
| Max file size (Enterprise) | 500 MB |
The platform automatically enforces upload rate limits per tenant. Default: 60 uploads per minute.
Reacting to Uploads
Every upload — whether via put(), a token upload, or a presigned URL — fires a storage.put event automatically. React to it with an event handler — run any code you like when a file lands — or let Maravilla run media transforms automatically via a declarative transforms block in maravilla.config.ts.
// events/on-photo-upload.ts
import { onStorage } from '@maravilla-labs/platform/events';
export const generateThumb = onStorage(
{ keyPattern: 'uploads/photos/*', op: 'put' },
async ({ key, platform }) => {
await platform.media.transforms.resize(key, { width: 400, format: 'webp' });
},
);
Next Steps
- Platform Services Overview — every service available to your project
- KV Store API Reference — for key-value storage
- Database API Reference — for document queries
- Media Transforms — transcode, thumbnail, resize, OCR uploaded files
- Event Handlers — react to uploads and other platform events