File storage

Every project has a blob store alongside its database. Uploads use multipart/form-data. All other operations use JSON metadata or raw byte streams.

The JavaScript examples below use the smoldb-js SDK:

import { smoldb } from "smoldb-js";
 
const db = smoldb({
  url: "https://your-smoldb.com",
  projectId: "<projectId>",
  key: "smol_...",
});

Uploading

POST /files with a single file field in a FormData body. The SDK builds the form when passed a File or Blob.

const { data, error } = await db.files.upload(file); // File from <input type="file">
// data: { id: "<fileId>" }
curl -X POST $SMOLDB_URL/files \
  -H "x-api-key: $KEY" \
  -F "file=@photo.jpg"

Warning. When calling fetch directly, do not set Content-Type manually. The runtime must pick the multipart boundary. Passing Content-Type: multipart/form-data without a boundary prevents the server from parsing the body and the upload fails silently. The SDK handles this correctly.

In a browser, a File from <input type="file"> can be passed through directly:

async function onChange(e) {
  const file = e.target.files[0];
  await db.files.upload(file);
}

Listing

GET /files returns an array of metadata rows. It does not return blob bytes.

const { data: files } = await db.files.list();
// files: [{ id, name, size, created_at }, ...]

Downloading

GET /files/:fileId streams the raw bytes with Content-Disposition: attachment and X-Content-Type-Options: nosniff. The server never renders uploaded bytes inline — a malicious user could otherwise upload a text/html file and weaponise it as stored XSS against anyone who opens the URL.

Practical consequences:

  • <a href={url}> will download the file rather than preview it in a new tab.
  • <img src={url}> still renders for image MIME types (browsers honour attachment headers only for top-level navigation).
  • To preview a PDF or text file in-app, fetch the bytes and render from a Blob URL:
const res = await fetch(
  `https://your-smoldb.com/api/projects/<projectId>/files/${fileId}`,
  { headers: { "x-api-key": "smol_..." } },
);
const blob = await res.blob();
const previewUrl = URL.createObjectURL(blob); // safe: object URL, not same-origin HTML

Programmatic fetches must pass the API key.

Deleting

DELETE /files/:fileId removes the blob and the metadata row in a single request.

const { error } = await db.files.delete(fileId);

Limits

| Plan | File storage | |------|--------------| | free | 200 MB | | pro | 2 GB |

Uploads can fail with two quota statuses:

| Status | Condition | |--------|-----------| | 413 | The single file is larger than the plan limit. | | 403 | Total storage after this upload would exceed the plan limit. |

Both are surfaced as error on the SDK's { data, error } return value.