--- title: 'Storage' description: 'S3-compatible object storage in your own box: the files your agents write, the documents you upload, browsed and shared from Studio with signed URLs.' position: 3 --- # Storage > Every workspace has an S3-compatible bucket of its own: MinIO under > `stackbone dev`, whatever bucket you point the deployed box at in production. > Your code reads and writes it through `stackbone.storage`, with keys the > runtime prefixes per agent so two agents never touch each other's files. The > **Storage** screen in Studio browses that same bucket, uploads into it, and > hands any object out with a **signed URL** that expires. Every key your code writes lands under the agent's own namespace, so an agent that writes `reports/q3.md` into the `exports` bucket ends up at `/exports/reports/q3.md` in the physical bucket. The explorer shows the part after the agent's prefix, and the SDK refuses a key with `..` in it, so a key an end user typed cannot escape that namespace. ## Browse, upload and share from Studio Open **Storage** under Data. The screen is a file explorer over the bucket: walk into a folder, create one with **New folder**, drop files onto the list or click **Upload file**, and **Delete** an object. Click an object and a panel opens with its key, a preview when it is text, and a signed URL you mint with a time to live. ![The Storage screen in Studio: the breadcrumb root / fs / sessions / a session id / notes, one file q3-summary.md, and a side panel with the object key, a signed URL generated with a 3600-second TTL, its expiry, and a text preview of the file.](/images/storage/object-panel.png) _A file the agent wrote during a conversation, opened from Studio: the key, a signed URL that expires in an hour, and the content._ ![The Storage screen showing the exports folder with two files uploaded from the browser, pricing-q3.md and refunds-2026-08.csv, with their sizes and dates.](/images/storage/exports.png) _A folder created from the screen, with two files uploaded by hand._ A signed URL is how a file leaves the box: it points at the object in your own bucket, it works for the seconds you chose, and it needs no credential from whoever opens it. Under `rag/` the screen only lists and downloads. Uploading, overwriting or deleting there would leave a document with no chunks behind it, so only the RAG screen writes to that folder. Uploading, creating a folder and deleting take the `member` role or above. Every role, a `viewer` included, can browse the bucket and mint a signed URL. ## From your code Inside a tool or a workflow step, pick a logical bucket and use it. The runtime injected the credentials when the box booted, so there is no client to build: ```ts import { stackbone } from '@stackbone/sdk'; async function publishReport(markdown: string) { 'use step'; const exports = stackbone.storage.from('exports'); const saved = await exports.upload('reports/q3.md', markdown, { contentType: 'text/markdown' }); if (saved.error) throw new Error(saved.error.code); // Hand the file to a person without streaming it yourself. const link = await exports.getSignedDownloadUrl('reports/q3.md', { expiresIn: 300 }); if (link.error) throw new Error(link.error.code); return { url: link.data.url }; } ``` Two patterns come up often. A tool that needs a file **from** the user mints a signed **upload** URL and returns it, so the user's browser puts the bytes straight into the bucket and the agent never carries them. A workflow that produces a report writes it here and returns the signed download URL as its result, which is what a run's **Final output** then shows in Studio. ## Where the bucket lives | Environment | Bucket | | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `stackbone dev` | A MinIO container started for you, with its bucket already made. Nothing to configure. | | The folder `stackbone package` writes | A MinIO in the same Compose file. A one-shot container creates the bucket, and the agent waits for it before it serves. Or point `.env` at your own bucket. | | A box you run your own way | The `STACKBONE_S3_*` variables on the container. | Any S3-compatible backend works (AWS S3, Cloudflare R2, Azure Blob, Google Cloud Storage, Railway Buckets, another MinIO). Create the bucket yourself first: only the bundled MinIO gets one made for it. One variable decides the address form. `STACKBONE_S3_FORCE_PATH_STYLE` defaults to `true`, which puts the bucket in the path (`https://host/bucket/key`) and is what MinIO and Cloudflare R2 accept. Set it to `false` for a backend that only answers to the subdomain form (`https://bucket.host/key`), such as AWS S3 and Railway Buckets. The wrong value fails every call with `NoSuchBucket` or a bare 403 while the credentials are fine, so check it before you suspect the keys. See [path-style or subdomain](/docs/sdk/data/storage#path-style-or-subdomain). > [!NOTE] > Check the [storage sdk](/docs/sdk/data/storage) and [storage cli](/docs/cli/reference/storage) documentation for more details. ## Read more - [`stackbone.storage`](/docs/sdk/data/storage): upload, download, list, remove, public and signed URLs, configuration and errors. - [Retrieval](/docs/sdk/data/rag): the documents behind the `rag/` folder. - [Agents](/docs/sdk/agents/overview): the file tools a deep agent gets, and where they write. - [`stackbone package`](/docs/cli/reference/package#what-you-set-on-the-deployed-container): the Compose bundle with MinIO and its bucket in it, and every variable you set on the deployed container.