Somewhere to put everything people upload.
Photos, PDFs, CSVs, recordings. Every project gets its own private file storage the moment it exists, with no storage account to open and no bucket policy to write. Included on every plan, including free.
You ask in English. It writes the upload.
Nothing gets provisioned first. Your agent writes a handler that takes the file, and a second one that hands it back later. These are the shapes the platform teaches it.
import { storage, db } from 'hatchable';
import crypto from 'node:crypto';
export const methods = ['POST'];
export const access = 'member';
export default async function (req, res) {
// multipart posts arrive already parsed:
// { field, filename, contentType, buffer }
const file = (req.files || []).find(
f => f.field === 'photo'
);
if (!file) return res.status(400).json({ error: 'No file.' });
if (file.buffer.length > 10 * 1024 * 1024) {
return res.status(413).json({ error: 'Max 10 MB.' });
}
const ext = file.filename.split('.').pop().toLowerCase();
const key = `photos/${req.member.id}/${crypto.randomUUID()}.${ext}`;
await storage.put(key, file.buffer, file.contentType);
// store the KEY, never the URL
await db.query(
'INSERT INTO photos (member_id, storage_key) VALUES ($1, $2)',
[req.member.id, key]
);
res.json({ ok: true });
}
import { storage, db } from 'hatchable';
export const access = 'member';
export default async function (req, res) {
const { rows } = await db.query(
`SELECT id, storage_key
FROM photos
WHERE member_id = $1
ORDER BY id DESC
LIMIT $2`,
[req.member.id, 50]
);
// the key is permanent. the link is minted here,
// fresh on every render, and lives about an hour.
const photos = await Promise.all(
rows.map(async r => ({
id: r.id,
url: await storage.url(r.storage_key),
}))
);
res.json({ photos });
}
Everything a file needs once you have it.
Taking the upload is the easy part. What makes storage worth having is everything that happens to the file afterwards.
Files arrive already opened
An ordinary form with a file input posts to your app, and the handler receives each file as its field name, original filename, content type, and bytes. Nobody writes a multipart parser.
The customer picks a photo and hits send. Your code gets the photo.
Private by default, not by configuration
There is no permanent public address for a stored file, and no setting that would give one out by accident. Every read is a link your app chose to mint, or bytes your handler fetched itself.
Nobody stumbles into your uploads by guessing a filename.
Links that expire on purpose
Ask for a link and you get one good for about an hour, or up to a week if you say so. Store the key in your database and mint the link at render time, so a copied URL goes dead on its own.
A link pasted into a group chat last month opens nothing today.
One address that never rots
When something outside your app caches the URL, a preview image in a chat app or a link in a feed, ask for a stable link instead. It never expires. Each visit is quietly handed fresh bytes.
The preview card on a shared post still loads a year later.
Your project's files, and only yours
Every key you use is filed inside your project's own space. Another project cannot read one of your files, cannot list what you have, and cannot write over it, whoever built it.
Two apps can both keep a file called logo.png and never meet.
Files that clean up after themselves
Anything filed under tmp/ is swept about a week after it is written. No cron job, no cleanup code, no bookkeeping. Use it for thumbnails, previews, and anything you can make again.
Generated previews pile up all month and take themselves out.
A file list without a database
Attach a few details when you store a file, then page through everything under a folder and read those details straight back. Some apps need no tables at all.
An image tool ships with a gallery and an empty database.
Reach the files from the chat
The same connection that built your app can store, fetch, list, and delete files directly, before and after launch. Tidying up does not need a screen to click through.
"Delete every test image I uploaded yesterday" is a sentence, not a chore.
The spec sheet, in plain terms.
storage.put, get, url, urlPermanent, list, head, del.storage.get inside your handler.tmp/ are swept about seven days after they are written. Everything else is kept until you delete it.storage.list: 100 files a page by default, 1,000 at most, with each file's own details on request.Numbers current as of August 2026. The plan allowances come from the same table the pricing page renders from and enforcement reads.
Say it like this.
You never mention storage, keys, or links. These are ordinary asks that turn into upload handlers, stored files, and pages that show them.
Asked and answered.
Do I need an S3 account or a storage provider?
No. Every project gets its own private file storage the moment it is created. There is no bucket to make, no access keys to paste, no policy to write, and no second bill. Your app calls storage.put and the bytes are stored.
Are my files public?
No. Storage is private end to end. There is no permanent public URL for a stored file. Every read goes through a short-lived signed link your app mints, or your handler reads the bytes itself and decides what to do with them.
How do people upload from a browser?
An ordinary form with a file input. The browser posts it, and your handler receives the file already parsed: the field name, the original filename, the content type, and the bytes. You do not write a multipart parser.
The link I got back stopped working. Why?
Signed links expire on purpose, one hour by default. Store the key rather than the URL, and mint a fresh link whenever you render the file. For a link that a third party will cache, like a preview image in a chat app, ask for a stable link instead and it will keep working.
What are the storage limits?
Storage is a plan allowance, not a per-file charge. Free includes 1 GB counted across your whole account, Builder raises that to 5 GB, and an app on the App plan gets 25 GB of its own, with another full share for every capacity block you add.
Can I get my files back out?
Yes. Keeping your data reachable is a commitment, not a feature. Your agent can list every stored object and pull the bytes for any of them, so a full copy of what your app is holding is one ask away.
More detail lives in the SDK reference and the pricing table.
Stop shopping for a bucket.
Connect the AI you already use, describe the app you want, and the place to put files is already there, private and wired in.
Want the technical surface? The SDK reference covers storage.put, signed links, and the rest.