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.

— what your AI actually does

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.

you, to your AILet people attach a photo to each report
api/upload.js
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 });
}
api/photos.js
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 });
}
the row holds the key. the page holds the link. one of those is allowed to expire.
— what you get

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.

Uploads

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

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

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.

Stable links

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.

Isolation

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.

Expiry

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.

Listing

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.

From your AI

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 numbers

The spec sheet, in plain terms.

What it isObject storage on S3, with a private space of its own for every project.
IncludedEvery plan, from the moment a project is created. The free plan includes it.
From codestorage.put, get, url, urlPermanent, list, head, del.
ReadsNo permanent public URL. Every read is a signed link your app mints, or storage.get inside your handler.
Link lifetimeOne hour by default, seven days at most. A stable link never expires and redirects to fresh bytes on every visit.
Browser uploadsOrdinary multipart form posts. Each file reaches your handler as its field, filename, content type, and bytes. Set your own size cap in the handler.
Auto-expiryKeys that start with tmp/ are swept about seven days after they are written. Everything else is kept until you delete it.
ListingPage through a folder with storage.list: 100 files a page by default, 1,000 at most, with each file's own details on request.
File detailsA small set of your own fields can ride along on each file, up to 2 KB in total, readable back without a database.
Allowance1 GB on Free and 5 GB on Builder, counted across your whole account. 25 GB per app on the App plan, and every capacity block adds another full share. See the plans →

Numbers current as of August 2026. The plan allowances come from the same table the pricing page renders from and enforcement reads.

— things people ask for

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.

upload"Let people attach a photo when they report a problem"
attachments"Customers should be able to send us a PDF with the form"
gallery"Show me everything I've uploaded, newest first"
exports"Save the weekly CSV somewhere I can download it later"
sharing"Give me a link to this image I can paste into Slack"
cleanup"When someone deletes a report, delete its photo too"
— common questions

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.

— free to start, no card

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.