Storage

The Storage service provides object storage for files of any size. It supports direct server uploads, presigned URLs for browser-to-storage uploads, download URL generation, and file metadata. In development, the CLI stores files locally. In production, Maravilla Cloud handles everything. Your code works identically in both environments.

import { getPlatform } from '@maravilla-labs/platform';

const platform = getPlatform();

await platform.env.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/key
  • data (Uint8Array | ArrayBuffer) — the file content
  • metadata (object, optional):
    • contentType (string) — MIME type
    • filename (string) — original filename
    • tags (object) — custom tags
await platform.env.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.env.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.env.STORAGE.delete('uploads/photo.jpg');

list(prefix?, limit?)

Lists files in storage, optionally filtered by prefix.

Parameters:

  • prefix (string, optional, default: '') — only return files whose keys start with this prefix
  • limit (number, optional, default: 100, max: 1000) — maximum number of results
const files = await platform.env.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.env.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 under
  • contentType (string) — the MIME type of the file being uploaded
  • size (number) — maximum allowed file size in bytes

Returns: { url, method, headers }

const uploadUrl = await platform.env.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 key
  • expiresInSeconds (number, optional) — seconds until the URL expires (default: 900 / 15 minutes)

Returns: { url, method, headers, expiresIn }

const download = await platform.env.STORAGE.generateDownloadUrl(
  'reports/q1.pdf',
  3600 // 1 hour
);

// Redirect user to download.url

Upload Patterns

Best for large files. The browser uploads directly to storage, reducing server bandwidth and improving performance.

Step 1: Server generates the presigned URL

// 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.env.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 directly to storage

async function uploadFile(file) {
  const key = `uploads/${Date.now()}-${file.name}`;

  // Get presigned URL from your server
  const res = await fetch('/api/uploads/generate-token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      key,
      content_type: file.type,
      size_limit: file.size
    })
  });

  const { upload_url, method, headers } = await res.json();

  // Upload directly to storage
  const uploadHeaders = new Headers();
  if (headers) {
    Object.entries(headers).forEach(([k, v]) => {
      uploadHeaders.set(k, v);
    });
  }

  await fetch(upload_url, {
    method: method || 'PUT',
    headers: uploadHeaders,
    body: file
  });

  return key;
}

Server Upload

Best for small files or when you need to process the file before storing it (validation, resizing, transformation).

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 = formData.get('key');

  // Validate
  if (file.size > 10 * 1024 * 1024) {
    return json({ error: 'File too large' }, { status: 400 });
  }

  // Store with metadata
  const metadata = {
    filename: file.name,
    content_type: file.type,
    size: file.size,
    uploaded_at: new Date().toISOString()
  };

  await platform.env.STORAGE.put(key, await file.arrayBuffer(), {
    contentType: file.type,
    filename: file.name,
    metadata
  });

  return json({ success: true, key, metadata });
};

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.env.STORAGE.put(key, fileData, {
  contentType: file.type,
  filename: file.name
});

// Store metadata in KV for quick access
await platform.env.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.env.STORAGE.generateUploadUrl(
  key,
  contentType,
  {
    sizeLimit: 10 * 1024 * 1024, // enforce size limit
    expires_in: 300               // 5 minutes
  }
);

Environment Details

SettingValue
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.

Next Steps