Files

Upload, sealed and public files, attachments in a group chat, and what happens if.

Files

const file = await tasks.upload(imageBlob, { name: "avatar.png" })
// { id, ref, contentType, sizeBytes } — store file.ref in a record field

const { url, expiresAt } = await g.files.link(file.ref)
// the only way a file becomes a URL, checked against the safety rule every time

upload() is for the files people upload:

  • It accepts images (JPEG, PNG, WebP, GIF, HEIC) and documents (PDF, ZIP, EPUB), up to 25 MB per file.
  • It accepts audio (mp3, m4a, wav, webm, ogg, flac, aac) up to 100 MB and video (mp4, webm, mov) up to 500 MB, in every collection. In Development the caps are 25 MB and 50 MB.
  • Nothing else is accepted: no SVG or HTML (zip an office file).
  • The server checks the file's actual bytes, not its name, and never converts them: no transcoding, no thumbnails.
  • Audio and video play in the page: put g.files.link(ref)'s URL in <audio> or <video controls>. They stream and seek, and are private like every file unless the collection is one anyone can read.
  • Per person: 20 audio or video uploads an hour; 5 uploads of any file in progress (started, not confirmed), holding at most 1 GB; and a storage share of the larger of 10% of the app's storage or 2 GB.
  • A document always downloads: it is served as an attachment and never opens inside your page. Link it with { intent: "download" }.

A reference is not a URL. Files in collections anyone can read without signing in resolve to permanent CDN URLs on your app's own subdomain. Everything else resolves to a short-lived signed link, checked against the collection's safety rule every time a link is created.

On addressed and direct collections, upload(blob, { for: userId }) hands the file to that one person. The recipient is stamped at upload and never changes, and only they (and you) can ever resolve it. That's how a direct message carries an attachment, and how an app hands one client a document.

Attachments in a group chat

What it is. One file every member of a group chat can open. A group message names up to 20 people; its attachment names the same people.

What it does.

  • Names the group. On a direct collection, upload(blob, { for: [id, id, …] }) takes up to 20 people, the same list create(data, { for: [ids] }) takes.
  • Opens for each of them. Every person named, and the uploader, can link() it and put its ref in their own messages. Everyone else is refused, as if the file did not exist.
  • Counts once. It is one file, held against the uploader's storage share.

What it does not do.

  • Change who can open it. The list is stamped at upload; to add someone, upload it again with the new list.
  • Take a list anywhere but direct. An addressed file names one person.

When you need something else.

  • More than 20 people: upload into a shared collection, which every signed-in person can read.

Example. A photo in a group chat of three.

const group = [ben.id, cleo.id]
const photo = await g.collection("chats").upload(blob, { for: group })
await g.collection("chats").create({ text: "look", photo: photo.ref }, { for: group })
// Ben and Cleo each open it with g.files.link(record.data.photo)

A field that holds file references becomes a file field, image or document, from the files uploaded to it. File references are checked on every safety rule, locked or not: a made-up ref is refused (unknown_file). Once a live collection's fields are fixed, attaching the wrong kind is refused too (invalid_shape, whose message states which kind the field takes). Until then, the kind you upload sets the field's kind.

AI runs have their own file limits:

  • An AI run's own output can be image, audio or video, up to 200 MB each, because the platform writes those files itself.
  • A transcribe tool's audio input is one of: audio the person uploads to that tool with g.ai.upload(tool, file), a sealed file ref they already own, or an https URL.
  • g.ai.upload takes mp3, m4a, wav, webm, ogg and flac, at 25 MB per file (14 MB on Google). The upload is a sealed file owned by the person who uploaded it, and is deleted after 24 h if no run used it.

What happens if…

If…What happensWhat you do
Someone uploads to a collection they can't write toThe upload is refused, exactly as a record write there would be. Upload permission is the collection's write ruleNothing
Your human makes a collection private or locks it to a planIts files become private: their public links stop opening, and link() hands short-lived signed linksStore the file reference, never the URL
Development rule changes that move files pass 5 a day, or 2 GiB of files a day, for one appA change that makes files public is 429 development_capped and the rule stays as it was; making a collection more private is never refused, and Live is never refusedTry again after resetAt, or test with npx gemmein dev
The file is SVG, HTML, an office file, or an audio or video format not listed (avi, mkv)415 unsupported_file_type. An empty file is 400 invalid_fileZip an office file; convert media to mp4, webm, mov or a listed audio type
An image or document is over 25 MB413 file_too_large, and the message states the capNothing
Audio is over 100 MB or video over 500 MB (25 MB and 50 MB in Development)413 file_too_large, naming the type and the environment. A file declared small but uploaded big is refused at confirm and discardedNothing
One person starts more than 20 audio or video uploads in an hour429 media_rate_limited with resetAt. Images, documents and other people are unaffectedTry again after resetAt
You have too many uploads in progress: one person with 5 files of any type started and not confirmed, or 1 GB of themThe next is 429 too_many_open_uploadsConfirm one, or wait an hour for them to expire
The bytes sent are larger than the size the upload declaredThe upload itself is refused: the link admits only the declared size. Confirming it is 400 upload_incompleteDeclare the real size
One person's files would pass their share: the larger of 10% of the app's storage or 2 GB, counting uploads in progress429 person_storage_full. Other people keep uploadingDelete some of that person's files
Development passes 50 audio or video uploads in 24 hours for one app429 development_capped with resetAt. Live has no daily countTry again after resetAt, or test with npx gemmein dev
An upload is never confirmedIt holds its declared size against that person's share, never the app's storage, for one hour, then lets go. Confirming it later is 400 upload_incomplete. The bytes are never served and are deleted within a dayUpload the file again
A Blob you built from bytes has no type415, and the message says none was declaredPass { contentType } to upload()
The bytes are not the type the file claims (a renamed file, such as a picture called .mp4)400 invalid_file_content. The upload is discarded and never served or playedSend the real file; a retry gets the same answer
A record field holds a made-up ref, or another app's400 unknown_file on create and updateStore only refs that upload() returned
You link a file in a collection anyone can readA permanent URL on your app's own subdomainNothing
You link any other fileA signed link that works for 2 minutes, or 5 minutes with { intent: "download" }Call link() when you render; never store the URL
The person may not see the filelink() is refused. A file that isn't theirs answers 404, as if it did not exist. A missing plan or product answers 403 entitlement_required with the plan's keyShow a sign-in or upgrade prompt
Access is revoked after a link was issuedThe next link() is refused. A link already issued works until it expires, and a copy already downloaded stays downloadedNothing
A file is uploaded with { for: userId }That person and the uploader can link it; everyone else is refused. for works on addressed and direct collections onlyNothing
A file on a direct collection is uploaded with { for: [id, id, …] }Every person named and the uploader can link it, and put its ref in a record; everyone else is refusedNothing
The list names more than 20 people400 too_many_recipients, and nothing is uploadedUpload into a shared collection
A list is sent to an addressed, private or other collection that is not direct, or the list is empty400 invalid_recipientsName one person, or upload into a direct collection
Someone on the list is not a person of this app, or is suspended400 invalid_audience, and nothing is uploadedFix the list
The collection is reply-only and someone on the list (or the one person named) has not written to the uploader first403 reply_only, the same answer the message gets, and nothing is uploadedWait for them to write first
Messages are turned off for the collection403 sends_disabledNothing
One member of the group deletes their accountThey are removed from the file's list. The uploader and the other members keep itNothing
The uploader deletes their accountThe file is deleted for everyone, as all their files areNothing
Your app's stored files reach 80% and then 100% of its storageYou get one email at each. On Live, uploads keep working for the lesser of 10% over or 7 days past it (in Development, up to twice the cap), checked with each file's actual size at confirm, then are refused with 429 usage_limit_exceededAdd a 50 GB storage pack or move the app up a tier on Usage & billing, or delete files
Was this page helpful?

Last updated