Guides

Storage

Upload and fetch project files from defineApi handlers. Visibility is a per-file public flag.

Every Starbase project has file storage. Apps write files the way they send email: a defineApi handler receives bytes (typically multipart) and calls the injected storage helper. Starbase holds the object-store credentials. You never configure a bucket or see raw keys.

Write, list, and delete files from injected storage in a defineApi handler, or from the org-member dashboard. api/storage.ts is a normal handler at /api/storage. Only GET /storage/objects/{key} is reserved, so <img src> and public paths keep working.

defineApi storage

Every handler receives storage next to db and email. Typical app upload is multipart defineApi plus ctx.storage.upload. Overwrite of the same key is allowed. A missing key throws (the handler 500s unless you catch), same as db.

api/planets.ts

import { defineApi, z } from "@robodev-ai/sdk";
export const post = defineApi({
auth: "required",
multipart: true,
body: z.object({
name: z.string().min(1),
}),
handler: async (ctx) => {
const file = ctx.files.find((f) => f.fieldname === "file") ?? ctx.files[0];
if (file) {
await ctx.storage.upload(`planet-images/${crypto.randomUUID()}`, file.data, {
public: true,
contentType: file.mimetype,
});
}
return { ok: true };
},
});
  • storage.upload(key, data, { public?, contentType? }) — Buffer, Uint8Array, or string. Default private, application/octet-stream.
  • storage.get(key) — { body, contentType, public }.
  • storage.getUrl(key, { expiresIn? }) — stable public project-host URL, or a short-lived signed project-host URL for private files (default 15 minutes, max 1 hour). Never an object-store hostname.
  • storage.delete(key) and storage.list({ prefix?, limit?, offset? }). list and upload object.url is always the stable project-host path.

Public vs private

  • Public — stable URL https://{projectId}.robodev.povio.dev/storage/objects/{key}. Anonymous GET 302s to the bytes.
  • Private — same stable path 404s without a project-user Bearer or a valid exp+sig query (does not leak existence). getUrl and dashboard Copy URL return a 15-minute signed project-host URL.
  • Visibility is set at upload. To change it, delete and re-upload.

Serve-only route

GET /storage/objects/{key} is reserved on every project host, including projects that have never been deployed. The key is required and URL-encoded. api/storage/objects.ts and api/storage/objects/** are not registered. api/storage.ts deploys at /api/storage.

  • Public object — 302 without auth, with Bearer, or with a valid exp+sig.
  • Private object — 404 unless the request has a valid project-user Bearer or a valid exp+sig. 404 does not leak existence.

Keys, size, and access

  • Keys are 1–512 characters: A-Za-z0-9._- and /. No leading slash, no empty or .. segments.
  • Max 1 GiB per object, accepted through handler multipart and the dashboard.
  • Wrap storage in your own handlers for access control. The dashboard is org-member only.

Dashboard

Open /projects/:id/storage. Upload with a public/private switch (default private) and an optional key. Bytes go through Starbase — there is no browser PUT to the object store. Copy URL uses the stable public path or a 15-minute signed private project-host URL. Delete asks for confirmation.

Object store host

getUrl, list/upload JSON, and dashboard Copy URL never return the object-store hostname. Browsers only see project-host /storage/objects/{key} (plus exp and sig for private files). The 302 target may be an internal presigned GET.