# 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](database.md) 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, `rclone` or 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. Use `storage-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.

```js
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](data-security.md)). For instance, each user reads only their own folder:

```sql
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](backups.md)).

## 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. |

## See also

- [Database](database.md), [Data security](data-security.md), [Authentication](auth.md), [Backups](backups.md)
- [Cache and images](cache.md), [Limits by tier](limits.md), [Migrate from Supabase](from-supabase.md)
