Object storage
MarvX gives each workspace S3-compatible buckets for user uploads, media and backups. You manage buckets and files through the MarvX REST API, and file bytes move over short-lived presigned URLs, so they never pass through your server or ours.
API base
<your MarvX API URL>/v1
Auth
Bearer API token (mv_…)
URL lifetime
15 minutes
S3_ACCESS_KEY, S3_SECRET_KEY, S3_REGION, S3_BUCKET). Until then they return a "not configured" error. There are no per-bucket access keys: use presigned URLs from the API.Quick start
1. Create an API token
Go to Dashboard → Settings → API Tokens and create a token. It's shown once, so store it as a secret. Never commit it to git.
MARVX_API_URL=https://your-marvx-api.example.com
MARVX_TOKEN=mv_xxxxxxxxxxxxxxxxxxxx
MARVX_ORG_ID=your-workspace-id2. Set up a client
// Node.js 18+ (built-in fetch)
const API = `${process.env.MARVX_API_URL}/v1`;
const ORG_ID = process.env.MARVX_ORG_ID;
const headers = {
Authorization: `Bearer ${process.env.MARVX_TOKEN}`,
"Content-Type": "application/json",
};3. Find your workspace ID
Every storage call takes an org_id. List your workspaces to get it.
const orgs = await fetch(`${API}/orgs`, { headers }).then((r) => r.json());
console.log(orgs.map((o) => [o.id, o.name]));4. Create a bucket and upload a file
See Buckets and Upload files below.
Authentication
Send your API token as a Bearer token on every request to the MarvX API. Presigned URLs carry their own signature, so requests to them need no Authorization header.
Authorization: Bearer mv_xxxxxxxxxxxxxxxxxxxxTokens act as the user who created them, so you can only reach buckets in workspaces you belong to. Revoke a token from the same settings page at any time.
Buckets
Create as many buckets as you need in a workspace. Files are stored under that workspace and bucket, isolated from other workspaces.
org_idrequired | string | Workspace ID |
namerequired | string | Bucket name, e.g. uploads |
const bucket = await fetch(`${API}/storage/buckets`, {
method: "POST",
headers,
body: JSON.stringify({ org_id: ORG_ID, name: "uploads" }),
}).then((r) => r.json());
console.log(bucket.id); // use this ID in the file endpointsGET /v1/storage/buckets?org_id=… lists buckets and DELETE /v1/storage/buckets/{id} removes one.
Upload files
Uploading is two steps: request a presigned upload URL from MarvX, then PUT the file bytes to that URL with the same Content-Type.
POST /v1/storage/buckets/{id}/upload-url
org_idrequired | string | Workspace ID |
keyrequired | string | File path inside the bucket, e.g. uploads/photo.jpg |
content_type | string | MIME type. Send the same value as Content-Type on the PUT. |
import { readFile } from "node:fs/promises";
const BUCKET_ID = "your-bucket-id";
// 1. Ask MarvX for a presigned upload URL
const { upload_url } = await fetch(
`${API}/storage/buckets/${BUCKET_ID}/upload-url`,
{
method: "POST",
headers,
body: JSON.stringify({
org_id: ORG_ID,
key: "uploads/photo.jpg",
content_type: "image/jpeg",
}),
},
).then((r) => r.json());
// 2. PUT the file straight to storage (same Content-Type as above)
await fetch(upload_url, {
method: "PUT",
headers: { "Content-Type": "image/jpeg" },
body: await readFile("./photo.jpg"),
});Download files
Request a presigned download URL, then GET it. You can also hand the URL to a browser or another service. It works without credentials until it expires.
POST /v1/storage/buckets/{id}/download-url
org_idrequired | string | Workspace ID |
keyrequired | string | File path inside the bucket |
import { writeFile } from "node:fs/promises";
const { download_url } = await fetch(
`${API}/storage/buckets/${BUCKET_ID}/download-url`,
{
method: "POST",
headers,
body: JSON.stringify({ org_id: ORG_ID, key: "uploads/photo.jpg" }),
},
).then((r) => r.json());
const file = await fetch(download_url);
await writeFile("photo.jpg", Buffer.from(await file.arrayBuffer()));List files
Returns every file in the bucket with its size and last-modified time, plus the bucket's total size.
GET /v1/storage/buckets/{id}/files?org_id=…
const { files, total_bytes } = await fetch(
`${API}/storage/buckets/${BUCKET_ID}/files?org_id=${ORG_ID}`,
{ headers },
).then((r) => r.json());
files.forEach((f) => console.log(f.key, f.size_bytes, f.last_modified));
console.log("Total:", total_bytes, "bytes");Delete files
Permanently remove a file from a bucket. This cannot be undone.
DELETE /v1/storage/buckets/{id}/files?org_id=…&key=…
const params = new URLSearchParams({ org_id: ORG_ID, key: "uploads/photo.jpg" });
await fetch(`${API}/storage/buckets/${BUCKET_ID}/files?${params}`, {
method: "DELETE",
headers,
});Presigned URLs
Upload and download URLs are signed for one object and expire after 15 minutes (expires_in_seconds: 900). Request a fresh one each time you need it rather than storing it.
// POST /upload-url
{ "upload_url": "https://…", "key": "uploads/photo.jpg", "expires_in_seconds": 900 }
// POST /download-url
{ "download_url": "https://…", "expires_in_seconds": 900 }Common patterns
- Browser uploads: your backend requests an upload URL and the browser PUTs the file directly to it
- Private downloads: generate a download URL when a signed-in user asks for a file
- Share a file with another service for a short window without giving it your token
Uploading directly from a browser requires CORS to be allowed on the S3 bucket the MarvX server is configured with.