Storage
Every project with a database has file storage, organised in buckets, served at the project address in the storage-js format. This page explains how to use it, how access to files is decided, what each tier allows, and what is not served yet, starting with the S3-compatible endpoint.
What is served
Storage is served under https://<identifier>.donnees.bastide.app/storage/v1 (see Database for the address). Your application keeps supabase-js: paths, bodies and responses are those storage-js calls.
storage-js call | Served |
|---|---|
createBucket, getBucket, listBuckets, updateBucket, deleteBucket, emptyBucket | yes |
upload, update, download, list, remove, move, copy, info, exists | yes |
createSignedUrl, createSignedUrls, createSignedUploadUrl, uploadToSignedUrl | yes |
getPublicUrl | yes |
| Resumable upload (tus) | yes |
Image transformation (/storage/v1/render/image/…) | yes, if the installation runs the rendering service; otherwise 501 |
S3-compatible endpoint (/storage/v1/s3) | no: 404, code chemin_inconnu (LIM-07) |
Note. The S3-compatible endpoint (for the AWS SDK,
rcloneor any S3 client) is not served yet. It is planned for batch L2; serving half of it would have applications written against a surface that will change. Usestorage-js, or the HTTP API it calls.
There is no console screen for buckets and files, and no MCP tool: storage is driven from your application. The Données (Data) screen shows the /storage/v1 endpoint in the « Base PostgreSQL » card.
Buckets
A bucket is created with three settings:
- public: a public bucket serves its files without a key, through the public URL;
- maximum file size: the smaller of the bucket's and the tier's applies;
- allowed types: a list of MIME types,
image/*allows any image; the comparison ignores case and parameters. An empty list allows everything.
import { createClient } from "@supabase/supabase-js";
const supabase = createClient("https://<identifier>.donnees.bastide.app", "<publishable key>");
await supabase.storage.createBucket("avatars", {
public: false,
fileSizeLimit: "5MB",
allowedMimeTypes: ["image/*"],
});
await supabase.storage.from("avatars").upload(`${userId}/portrait.png`, file);
In practice, creating a bucket requires the secret key, server side, or a policy that allows it.
Who can read and write
Every request runs under the caller's role: the anonymous role with the publishable key alone, the authenticated role with a session token, the service role with a secret key. The policies on your database's storage.objects table decide; the platform holds no file access rule of its own. A refused read is refused by the policy, not by the application.
A storage policy is written like any other (see Data security). For instance, each user reads only their own folder:
create policy "read own folder" on storage.objects for select
using (bucket_id = 'avatars' and (storage.foldername(name))[1] = (select auth.uid())::text);
Three paths need no key, because a browser adds no header to an <img> tag or a form: the public URL of a public bucket, the signed read URL, and the signed upload URL. A signed URL's permission is checked when it is signed.
- A signed URL lives one hour by default, seven days at most; beyond that, 400
duree_refusee. - An upload is first tested against your policy: a refusal costs one request, not a file sent.
Limits by tier
| Tier | Maximum file size | Included storage |
|---|---|---|
| Discovery | 50 MB | 5 GB |
| Standard | 5 GB | 50 GB |
| Pro | 50 GB | 250 GB |
| Sovereign | no tier limit | 1 TB |
Sizes are counted in powers of 1,024. Nothing is billed beyond the included amount today: the included amount is the quota.
- At 80 % of the quota, the crossing is recorded in the project's audit log.
- At 100 %, any new write is refused (413,
quota_atteint) with a message naming the quota; reads carry on.
Clean-up
Every day, one pass per project removes what is no longer used: a resumable upload nobody has fed for 24 hours, a multipart upload abandoned for 24 hours, and bytes no object has pointed to for an hour. Each pass that removes something is recorded.
Backing up files
The files in your buckets are not restored with the database: a database restore brings back the storage.objects rows as of the dump, not the bytes. Keep a copy elsewhere of anything that cannot be rebuilt (see Backups).
Common errors
| Code | Status | Cause and remedy |
|---|---|---|
refuse_par_la_politique | 403 | A policy on storage.objects refuses this action to this caller. |
fichier_trop_grand | 413 | The file exceeds the bucket's or the tier's limit; the message says which. |
quota_atteint | 413 | The project's storage is full; delete files or change tier. |
type_refuse | 415 | The bucket does not allow this type; the message lists the allowed types. |
seau_introuvable | 404 | The bucket does not exist, or the policy does not let you see it. |
existe_deja | 409 | An object already has this name; upload with upsert: true to replace it. |
nom_refuse | 400 | The object name is not allowed. |
duree_refusee | 400 | The requested signed URL exceeds seven days. |
chemin_inconnu | 404 | The path is not served, for instance the S3 endpoint. |