Aller au contenu

Configuration du stockage

Rebase prend en charge trois backends de stockage :

  • Système de fichiers local — Fichiers stockés sur disque (idéal pour le développement)
  • Compatible S3 — AWS S3, MinIO, Cloudflare R2, DigitalOcean Spaces
  • Google Cloud Storage / Firebase Storage — Prise en charge native de GCS via @google-cloud/storage

Le stockage est configuré dans le bloc storage de initializeRebaseBackend :

const backend = await initializeRebaseBackend({
// ...
storage: {
type: "local",
basePath: "./uploads" // Directory for file storage
}
});

Le stockage local conserve les métadonnées de chaque objet à côté de celui-ci, sous la forme <key>.metadata.json ; ainsi, une clé dont le dernier segment se termine par .metadata.json est refusée avec 400 INVALID_STORAGE_KEY sur toutes les routes. S3 et GCS conservent les métadonnées sur l’objet et stockent n’importe quelle clé. Sur un système de fichiers insensible à la casse (macOS et Windows par défaut), Avatar.png et avatar.png représentent un seul fichier local mais deux objets sur S3, GCS ou un disque Linux ; le serveur émet donc un avertissement au démarrage lorsque le dossier de stockage se trouve sur un tel système.

const backend = await initializeRebaseBackend({
// ...
storage: {
type: "s3",
bucket: env.S3_BUCKET!,
region: env.S3_REGION || "auto",
accessKeyId: env.S3_ACCESS_KEY_ID || "",
secretAccessKey: env.S3_SECRET_ACCESS_KEY || "",
endpoint: env.S3_ENDPOINT, // For MinIO, R2, etc.
forcePathStyle: env.S3_FORCE_PATH_STYLE // Required for MinIO
}
});
const backend = await initializeRebaseBackend({
// ...
storage: {
type: "gcs",
bucket: env.GCS_BUCKET!,
projectId: env.GCS_PROJECT_ID,
}
});

Sur GCP (Cloud Run, GCE, GKE), les identifiants du compte de service par défaut sont utilisés automatiquement. En dehors de GCP, définissez la variable d’environnement GOOGLE_APPLICATION_CREDENTIALS avec le chemin vers votre fichier de clé de compte de service.

Vous pouvez configurer plusieurs backends nommés et router différents champs vers différents stockages :

storage: {
"(default)": { type: "local", basePath: "./uploads" },
"media": { type: "s3", bucket: "media-bucket", region: "us-east-1", ... }
}

Ensuite, dans les propriétés de votre collection, référencez un backend spécifique :

image: {
type: "string",
name: "Image",
storage: {
storagePath: "products",
storageSource: "media" // Routes to the "media" S3 backend
}
}
Méthode Chemin Description
POST /api/storage/upload Téléversement direct de fichier, un seul corps multipart/form-data : file (obligatoire) ; key, la clé d’objet, dossiers compris (author_pictures/ada.jpg), sinon le nom du fichier à la racine ; bucket, sinon le bucket par défaut ; storageId ; et metadata_<name> pour des métadonnées personnalisées. Tout autre champ est ignoré : il n’existe pas de champ path
POST /api/storage/upload?storageId=<key> Téléversement vers un backend nommé spécifique
GET /api/storage/file/* Récupérer un fichier — tout ce qui suit /file/ est la clé de l’objet
GET /api/storage/file/*?storageId=<key> Récupérer un fichier depuis un backend spécifique
GET /api/storage/metadata/* Taille, type de contenu et métadonnées personnalisées d’un objet, sans ses octets, plus un jeton de téléchargement à courte durée de vie lorsqu’il est privé
POST /api/storage/metadata Depuis la version 0.25 La même chose pour un maximum de 100 objets d’une même source : corps { keys, storageId? }
DELETE /api/storage/file/* Supprimer un fichier
GET /api/storage/list Lister les objets sous un préfixe (prefix, bucket, maxResults, pageToken, storageId). Un maxResults inférieur à 1, ou un pageToken jamais émis par la source, renvoie 400 INVALID_LIST_OPTIONS
POST /api/storage/folder Créer un marqueur de dossier vide
GET /api/storage/sources Les sources de stockage desservies par ce backend, par clé
OPTIONS /api/storage/tus Interroger les fonctionnalités supportées du protocole TUS
POST /api/storage/tus Initier une session de téléversement TUS avec reprise
HEAD /api/storage/tus/:id Vérifier la progression du téléversement (décalage en octets)
PATCH /api/storage/tus/:id Ajouter un fragment de données au fichier temporaire
DELETE /api/storage/tus/:id Terminer/interrompre la session de téléversement TUS

Ce qu’ils renvoient. Une enveloppe unique, identique à celle utilisée par /api/data : la charge utile se trouve sous data, et un échec est sous la forme { "error": { message, code, requestId } } avec les codes listés dans la référence des erreurs. /api/storage/file/* est l’exception, car sa charge utile est le fichier lui-même — il renvoie les octets, avec Content-Type, Content-Length et les en-têtes de mise en cache.

// GET /api/storage/list?prefix=products/images/
{ "data": { "items": [ { "bucket": "default", "fullPath": "products/images/a.jpg", "name": "a.jpg",
"size": 48213, "updated": "2026-10-01T12:00:00.000Z", "contentType": "image/jpeg" } ], "prefixes": [] } }

Depuis la version 0.25 Un listing décrit chaque fichier retourné : sa taille size en octets et updated, date de sa dernière écriture, sur le stockage local, S3 et GCS, ainsi que son contentType sur le stockage local et GCS. Un listing S3 ne transporte pas le type, il est donc absent sur S3 et /api/storage/metadata/* continue de le fournir. Un dossier dans prefixes n’en transporte aucun, tout comme un StorageController personnalisé qui ne les définit pas.

POST /api/storage/upload renvoie 201 avec { key, bucket, storageUrl } de l’objet stocké sous data ; GET /api/storage/metadata/* renvoie les métadonnées de l’objet et, pour un objet privé, le token à courte durée de vie ; POST /api/storage/metadata renvoie un objet indexé par les chemins reçus, chacun contenant ce que le GET aurait répondu (voir ci-dessous). Un dossier n’est pas un objet : /metadata sur une clé se terminant par / renvoie 404 sur tous les stockages, même là où S3 ou GCS conserve un marqueur folder/, aucun jeton de téléchargement n’est donc émis pour un préfixe ; GET /api/storage/sources renvoie le tableau des sources configurées, avec configured: true. Sur un déploiement sans aucun stockage configuré, il répond 200 {"data": [], "configured": false}, tandis que toutes les autres routes renvoient 501 STORAGE_NOT_CONFIGURED. GET /api/auth/config signale ce même état avec storage: false, permettant ainsi à un client d’éviter de poser la question. GET /api/storage/sources est public : il répond sans jeton même lorsque requireAuth est activé. Il énumère les sources (leur clé, moteur, transport et libellé) et jamais un fichier, et le SDK client le lit pour initialiser ses sources, ce qui peut se produire avant toute authentification. Une application qui lit anonymement une source direct en a également besoin. Chaque route qui liste, lit ou écrit un fichier conserve son authentification. DELETE /api/storage/file/* et POST /api/storage/folder ne transmettent qu’un message, puisqu’il n’y a rien à retourner.

Sur S3 et GCS, un bucket doit faire partie de ceux desservis par la source, tant en écriture qu’en lecture : un téléversement, un POST /api/storage/folder ou un téléversement TUS désignant tout autre bucket répond 404 UNKNOWN_STORAGE_SOURCE, tout comme le listing. Le stockage local crée quant à lui toujours un bucket lors de sa première écriture.

Plusieurs objets à la fois. Depuis la version 0.25 POST /api/storage/metadata accepte { "keys": ["photos/a.png", "photos/b.png"], "storageId": "media" } — jusqu’à 100 chemins d’objets d’une même source, chacun formaté comme GET /api/storage/metadata/<key> le requiert — et renvoie une entrée par clé :

{ "data": {
"photos/a.png": { "fullPath": "photos/a.png", "size": 48213, "contentType": "image/png", "token": "…", "tokenExpiresIn": 300 },
"photos/b.png": { "error": { "code": "FORBIDDEN", "message": "Not authorized for this object", "status": 403 } }
} }

Chaque clé est évaluée exactement comme le ferait le GET — le même hook storageAuthorize et portée de stockage, la même règle publique, son propre jeton de téléchargement — et une clé refusée, manquante ou mal formée répond avec sa propre error sans faire échouer ni révéler les autres. Un appelant sans session sur un déploiement qui en exige une reçoit le code 401 que le GET aurait renvoyé, sauf si toutes les clés sont publiques. Plus de 100 clés, ou un corps sans keys, renvoie 400 BAD_REQUEST. Le limiteur de débit (rate limiter) comptabilise la requête comme ⌈N/10⌉ requêtes pour N clés, ainsi une page de 100 miniatures en coûte 10. client.storage.getSignedUrl() et le panneau d’administration regroupent ainsi les résolutions d’une page de cette façon, et basculent sur un GET par fichier sur un serveur dépourvu de cette route.

Comment une lecture de fichier est autorisée. Les routes de lecture — /api/storage/file/* et /api/storage/metadata/* — acceptent le jeton signé à courte durée de vie émis par getSignedUrl(), transmis via ?token=<token> ou en tant que Bearer. Un JWT d’accès ordinaire est refusé sur /file/* avec 401 Unauthorized: Access JWT not allowed on file routes : le jeton fonctionnant sur toutes les autres routes ne fonctionne volontairement pas ici, car une URL de fichier est destinée à être transmise à un navigateur, un CDN ou une balise <img>. Toutes les autres lignes ci-dessus acceptent le JWT d’accès comme d’habitude.

Rebase inclut un pipeline de traitement d’images intégré propulsé par Sharp. Lors de la distribution de ressources images depuis le stockage, vous pouvez appliquer des opérations dynamiques via des paramètres d’URL :

# Serve image scaled to 300px width in webp format
GET /api/storage/file/products/laptop.jpg?width=300&format=webp
  • width, height : Limites de redimensionnement, 1–4096 (l’image n’est jamais agrandie).
  • quality : 1–100.
  • format : Convertit le format de l’image. Formats supportés : webp, jpeg, png, avif.
  • fit : cover, contain, fill, inside ou outside.

Un paramètre en dehors de ces limites renvoie un 400, et non une valeur discrètement tronquée — ?width=99999 renvoyait auparavant une image de 4096px et ?format=tiff du webp, sans qu’aucun message ne le signale.

Les miniatures d’images stockées dans le panneau d’administration utilisent ce point de terminaison : un aperçu sous forme de tableau ou de carte demande un rendu webp environ deux fois supérieur à sa taille affichée (96–800px) plutôt que l’original. Les fichiers SVG et GIF sont toujours affichés tels quels. sharp est facultatif : Depuis 0.25 sans lui, une demande de transformation reçoit l’original, et le backend avertit une fois de lancer pnpm add sharp dans backend/ (en 0.24, elle répond 500, et le panneau affiche les originaux).

La transformation consomme beaucoup de CPU et de mémoire, et sur un objet public, le point de terminaison est accessible de manière anonyme, de sorte que le travail est encadré plutôt que simplement mis en cache :

  • Capacité : un LRU plafonné à 500 entrées globalement, indexé par source de stockage, bucket et clé canonique.
  • TTL (Time to Live) : Les variantes mises en cache expirent après 1 heure.
  • Les requêtes concurrentes pour une même variante non mise en cache produisent une seule transformation, et non une chacune.
  • Un nombre restreint de transformations s’exécute simultanément ; au-delà d’une file d’attente délimitée, le serveur répond 503 TRANSFORM_OVERLOADED plutôt que d’accepter un travail qu’il ne pourra pas traiter.

Ce cache réside dans le processus, ce qui signifie qu’il n’est pas partagé entre les instances et ne survit pas à un redémarrage. Deux réplicas calculeront chacun chaque variante, et un déploiement invalidera l’intégralité du cache.

storageRenditionCache réécrit chaque image dérivée dans le même bucket que sa source, ainsi le travail est effectué une seule fois pour l’ensemble du déploiement plutôt qu’une fois par instance et par version :

storageRenditionCache: { enabled: true }

ou STORAGE_RENDITION_CACHE=true pour un déploiement sous forme de bundle. Les rendus sont stockés sous le préfixe réservé _rebase/renditions/, indexés par la version de l’objet source — ainsi, le remplacement d’une image distribue immédiatement la nouvelle version.

Trois éléments à connaître avant de l’activer :

  • Une lecture implique désormais une écriture. Chaque nouvelle variante coûte un PUT dans votre bucket. Cette option est désactivée par défaut pour cette raison.
  • Une écriture échouée n’est pas une requête échouée. Des identifiants en lecture seule, ou une politique de bucket refusant le préfixe, basculent sur le cache en mémoire ; l’image est toujours distribuée et la raison est consignée une seule fois dans les logs.
  • Les rendus obsolètes ne sont pas nettoyés. Remplacer un objet source laisse orphelins ses anciens rendus. Définissez une règle de cycle de vie sur _rebase/renditions/ — ce préfixe est fixe et non configurable, précisément pour qu’une règle puisse le cibler.

Le préfixe n’est pas accessible depuis l’API. Le lire ou y écrire directement renvoie 400 INVALID_STORAGE_KEY : chaque règle d’accès du produit — storageAuthorize tout comme les politiques déclaratives — est rédigée par rapport à la clé source, et un rendu distribué sous son propre chemin répondrait à une demande non sollicitée.

Le type de contenu stocké correspond à ce que le téléverseur a déclaré — rien n’analyse les octets — ainsi /api/storage/file/* n’affichera inline qu’une liste blanche stricte : images, vidéo, audio, application/pdf et text/plain. Tout le reste, text/html inclus, est distribué en tant que application/octet-stream avec Content-Disposition: attachment, et chaque réponse comporte X-Content-Type-Options: nosniff. Le stockage n’est pas un hébergeur web : une page téléversée rendue sur l’origine de l’API pourrait lire les cookies de cette origine et appeler ses points de terminaison.

Un SVG est une image qui est également un document pouvant contenir des <script>. Il est distribué en tant que image/svg+xml — afin de s’afficher dans une balise <img> et dans les miniatures du panneau d’administration — avec Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox sur chaque réponse, URLs transformées et URLs avec jeton de téléchargement incluses. Ouvert directement, il s’affiche avec les scripts désactivés et une origine opaque, ne pouvant ainsi pas accéder aux cookies ou aux points de terminaison de l’API.

Chaque source de stockage accepte des fichiers jusqu’à sa propre limite : 50 Mo sauf si STORAGE_MAX_FILE_SIZE (en octets, suffixé par __<KEY> pour une source nommée) ou le maxFileSize de la source n’en dispose autrement. Chaque point d’entrée limite un fichier à la même valeur. POST /api/storage/upload renvoie 413 PAYLOAD_TOO_LARGE en cas de dépassement ; un téléversement avec reprise est refusé dès sa création, avant son premier fragment ; et OPTIONS /api/storage/tus l’annonce sous l’en-tête Tus-Max-Size — pour la source indiquée par ?storageId=, ou celle par défaut sinon.

Sur la version 0.23, seul le maxFileSize de la source définit la limite ; STORAGE_MAX_FILE_SIZE n’est pas lue.

STORAGE_MAX_FILE_SIZE=209715200 # (default): 200 MB
STORAGE_MAX_FILE_SIZE__MEDIA=1073741824 # media: 1 GB

La quantité de données qu’un serveur garde en mémoire à la fois dépend du point d’entrée. POST /api/storage/upload constitue un corps multipart complet que le serveur traite en entier : trois à cinq fois la taille du fichier dans sa mémoire. Un téléversement avec reprise (TUS) ne conserve qu’un fragment à la fois. Depuis la version 0.25 client.storage.putObject() et le panneau d’administration envoient les fichiers de plus de 8 Mio via le protocole avec reprise. Consultez Téléversements avec reprise (TUS) pour découvrir le protocole, ce qu’il renvoie et son comportement à travers plusieurs instances.

Variable Description
STORAGE_TYPE "local", "s3" ou "gcs"
STORAGE_PATH Répertoire de stockage local (par défaut : <project>/uploads, sous rebase dev comme dans un serveur écrit par rebase eject). Un chemin relatif est lu depuis le répertoire où tourne le serveur, qui après rebase eject est backend/. Un bucket local nommé conserve ses fichiers à côté, dans <STORAGE_PATH>__<key>, sauf si STORAGE_PATH__<KEY> en définit un propre
S3_BUCKET Nom du bucket S3
S3_REGION Région AWS (par défaut : "auto")
S3_ACCESS_KEY_ID Clé d’accès AWS
S3_SECRET_ACCESS_KEY Clé secrète AWS
S3_ENDPOINT Point de terminaison S3 personnalisé (pour MinIO, R2)
S3_FORCE_PATH_STYLE Utiliser des URLs de type path-style (requis pour MinIO)
GCS_BUCKET Nom du bucket Google Cloud Storage
GCS_PROJECT_ID ID de projet GCP pour GCS
GCS_KEY_FILENAME Chemin vers un fichier de clé de compte de service GCP (à omettre sur GKE — Workload Identity/ADC fournit les identifiants)
GOOGLE_APPLICATION_CREDENTIALS Variable ADC standard, lue directement par le SDK Google (inutile sur GCP avec les identifiants par défaut)
STORAGE_DOWNLOAD_TOKEN_TTL Durée de validité de l’URL de téléchargement d’un fichier privé, en secondes — durée de vie du jeton émis par /api/storage/metadata/* (par défaut 300, au maximum 604800, soit une semaine). Augmentez-la pour les vidéos et audios privés, qui continuent de requérir des plages d’octets après le rendu de la page. Équivalent en variable d’environnement de storageDownloadTokenTtl
STORAGE_MAX_FILE_SIZE Fichier le plus volumineux accepté par la source, en octets (par défaut 52428800, soit 50 Mo). Suffixez par __<KEY> pour une source nommée. Une valeur qui n’est pas un nombre entier d’octets empêche le démarrage. Voir Taille maximale autorisée pour un fichier
FORCE_LOCAL_STORAGE Autoriser STORAGE_TYPE=local en production — voir ci-dessous
STORAGE_PUBLIC_READ Distribuer les objets stockés aux lecteurs non authentifiés. Équivalent en variable d’environnement de storagePublicRead, et l’un des trois moyens de satisfaire le garde-fou de démarrage en production.
STORAGE_ALLOW_ANY_AUTHENTICATED Désactiver le garde-fou de démarrage, restaurant le comportement où tout utilisateur connecté peut lire, écraser, supprimer ou lister n’importe quelle clé. Équivalent en variable d’environnement de storageInsecureAllowAnyAuthenticated. Ne se justifie que si chaque utilisateur connecté a la confiance nécessaire pour accéder à l’ensemble des fichiers.

Un projet peut comporter plus d’un bucket. Déclarez chacun d’eux dans config/resources.ts — le point unique lu par la plateforme, le runtime et la console :

import { bucket } from "@rebasepro/types";
export const uploads = bucket({ engine: "s3" }); // the default one
export const media = bucket("media", { engine: "s3", label: "Media" });

Exécutez ensuite rebase resources --write, ce qui régénère rebase.resources.json afin qu’un hôte puisse lire votre topologie sans avoir à exécuter un build. Consultez Sources multiples pour la gestion combinée des bases de données, buckets et topics.

Chaque source est configurée à partir des mêmes noms de variables portant leur propre suffixe. La source par défaut ne prend aucun suffixe, ainsi un projet à bucket unique continue d’utiliser les noms simples ci-dessus sans rien devoir déclarer, et déclarer des buckets nommés à côté ne les change pas (Depuis 0.25, détails) :

S3_BUCKET=app-uploads # (default)
S3_BUCKET__MEDIA=app-media # media
S3_ACCESS_KEY_ID__MEDIA=…
S3_SECRET_ACCESS_KEY__MEDIA=…

Le suffixe est dérivé de la clé : mis en majuscules, les caractères non alphanumériques transformés en underscores, précédés d’un double underscore (media-cdn → __MEDIA_CDN). Un simple underscore entrerait en collision avec de véritables noms de variables — S3_BUCKET_NAME serait interprété comme le bucket name.

Associez une propriété à une source à l’aide de storageSource :

{
name: "Cover",
dataType: "string",
storage: { storageSource: "media", acceptedFiles: ["image/*"] }
}

Une source déclarée mais jamais configurée est ignorée, sans être fatale : les téléversements qui lui sont adressés renvoient 501 STORAGE_SOURCE_NOT_CONFIGURED. La déclaration d’un bucket survient généralement avant qu’on ne lui associe un stockage, et une erreur de démarrage ici ferait crasher en boucle le backend en attendant une configuration. Une source que l’environnement configure incorrectement — un type sans bucket, ou un bucket sans identifiants — est refusée au démarrage, car il s’agit d’une erreur et non d’une simple omission.

Les identifiants décrivent généralement le fournisseur, et non le bucket. Quinze buckets sur une même installation MinIO nécessiteraient sinon quinze copies de la même clé d’accès, et une rotation impliquerait quinze modifications coordonnées. Nommez plutôt un compte :

export const media = bucket("media", { engine: "s3", account: "minio" });
export const avatars = bucket("avatars", { engine: "s3", account: "minio" });
S3_BUCKET__MEDIA=b-media # per bucket, always
S3_BUCKET__AVATARS=b-avatars
S3_ACCESS_KEY_ID__MINIO=… # shared by both
S3_SECRET_ACCESS_KEY__MINIO=…
S3_ENDPOINT__MINIO=https://minio.internal

Seules les variables délimitées au compte adoptent une valeur de repli — S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_ENDPOINT, S3_REGION, S3_FORCE_PATH_STYLE, et la paire GCS GCS_PROJECT_ID / GCS_KEY_FILENAME. Le nom du bucket n’a jamais de valeur de repli : c’est ce qui distingue une source d’une autre. Une valeur définie par bucket reste prioritaire, permettant ainsi à une source de changer de fournisseur sans impacter les autres.

Lors de l’utilisation de multiples backends de stockage, transmettez storageSources au fournisseur <Rebase> afin que le frontend sache comment router directement les téléversements :

import { Rebase } from "@rebasepro/app";
<Rebase
apiUrl="https://api.example.com"
storageSources={[
// `engine` names the provider, `transport` says who talks to it:
// "server" proxies through the Rebase backend, "direct" goes
// client-to-provider (and needs a `source` implementation).
{ key: "media", engine: "s3", transport: "server", label: "Media CDN" },
{ key: "firebase", engine: "firebase", transport: "server", label: "Firebase Storage" },
]}
>
{() => <MyApp />}
</Rebase>

La key de chaque source doit correspondre à une clé de backend enregistrée dans la configuration storage du serveur. Le contexte React StorageSourcesContext détermine la source active pour chaque champ de téléversement.

Chaque objet transite par le serveur, avec un ETag pour une revalidation peu coûteuse, un en-tête Cache-Control dépendant de qui peut le lire, et des plages d’octets pour la navigation dans les flux audio et vidéo. Mise en cache du stockage et CDNs détaille les en-têtes ainsi que la configuration à appliquer sur un CDN placé en frontal.

  • Montez un volume persistant sur le parent de STORAGE_PATH si vous utilisez le stockage local sur Docker/Kubernetes, et définissez FORCE_LOCAL_STORAGE=true — voir Auto-hébergement
  • Utilisez S3 ou un équivalent compatible (R2, MinIO), ou GCS, pour les déploiements en production
  • Configurez un CDN (CloudFront, Cloudflare) en frontal pour les performances — voir Placer un CDN en frontal
  • Toute application avec du stockage en production doit déclarer un modèle d’accès — voir ci-dessous. Cela ne concerne pas uniquement les architectures multi-locataires : le serveur refuse de démarrer sans cela.

La forme déclarative. Une liste de modèles de chemins, lus sans aucune exécution de code :

storagePolicies: [
{ path: "public/**", operations: ["read"], allow: "public" },
{ path: "users/:uid/**", allow: ({ params, user }) => user?.uid === params.uid }
]

Une clé ne correspondant à aucune politique est refusée. Chaque ouverture de droit est une ligne explicite, et une erreur entraîne un refus plutôt qu’un accès accordé.

Les modèles effectuent la correspondance par segment, jamais par sous-chaîne — public/** ne correspond pas à publicity/secret.png :

Modèle Correspond à
avatars/logo.png cette clé exactement
users/*/avatar.png exactement un segment là où se trouve *
users/:uid/** un segment capturé, puis le reste — y compris rien

** n’est autorisé qu’en tant que segment final. :name capture un segment et ne s’étend jamais sur un / ; les valeurs capturées sont transmises via params au prédicat.

allow peut valoir "public" (tout le monde), "authenticated" (tout appelant avec un uid), ou un prédicat recevant les paramètres capturés, l’utilisateur, l’opération et le bucket. operations prend par défaut les quatre valeurs — read, write, delete, list.

Les politiques satisfont le garde-fou de démarrage en production à elles seules, et un modèle mal formé fait échouer le démarrage plutôt que le premier téléversement.

requireAuth et publicRead sont des commutateurs globaux : ils déterminent si un appelant doit être connecté, et non ce à quoi il a accès. Sans hook d’autorisation, tout utilisateur authentifié peut lire n’importe quelle clé dont il connaît le nom — la seule protection entre les fichiers de deux locataires réside dans l’impossibilité de deviner les clés, ce qui ne constitue pas un modèle de contrôle d’accès. Pire encore, ils peuvent d’abord effectuer un GET /storage/list?prefix=, de sorte qu’il n’est même pas nécessaire de deviner les clés.

storageAuthorize est l’analogue pour le stockage des règles de sécurité d’une collection, et s’exécute après l’authentification sur chaque route de stockage :

await initializeRebaseBackend({
storage: { type: "s3", bucket: "app-files", /* ... */ },
storageAuthorize: async ({ key, bucket, operation, user }) => {
if (!user) return false;
// Keys are laid out as `{teamId}/{docId}/...`
const [teamId] = key.split("/");
return isTeamMember(user.uid, teamId);
}
});
Champ Description
key Clé de l’objet, débarrassée du préfixe de bucket et assainie des traversées de répertoires
bucket Bucket résolu ("default" lorsqu’il n’est pas spécifié)
operation "read", "write", "delete" ou "list"
user { uid, email?, roles? }, ou null lorsque la route autorise l’accès anonyme
storageId Le backend nommé, lorsque la requête en ciblait un — sa clé canonique (storageId=%20media vaut "media"). Absent pour la source par défaut, quelle que soit la façon dont la requête l’a nommé (aucun paramètre, vide, ou (default))
data Accès en lecture approuvé, contournant la RLS — data.collection(slug).find(query) / .findById(id). La notion de propriétaire réside dans une ligne de base de données, pas dans un préfixe de clé ; le hook a donc besoin d’un lecteur pour déterminer « qui possède cet objet ? ». Il contourne délibérément la sécurité au niveau des lignes : ce hook constitue la décision d’autorisation, et l’évaluer à travers un lecteur déjà restreint par les permissions propres de l’appelant créerait une circularité. Conçu en lecture seule par définition.

Renvoyez false pour refuser l’accès avec un code 403. Lever une exception refuse également l’accès — une recherche de propriétaire qui échoue ne laisse pas l’accès ouvert.

Bon à savoir :

  • La route de métadonnées est l’endroit où l’accès en lecture est réellement validé. Elle émet le jeton de téléchargement à courte durée de vie restreint au chemin auquel la route du fichier fait confiance, le hook contrôle donc l’accès à ce niveau. Deux lectures contournent le hook, par conception. Une requête portant un tel jeton : le hook l’a déjà approuvée lors de la création du jeton, pour ce chemin et cette source uniquement, de sorte qu’un hook qui refuserait désormais l’accès (une adhésion révoquée entre-temps) ne l’interrompt pas avant l’expiration du jeton — storageDownloadTokenTtl, 300 secondes par défaut. Et la lecture d’une clé sous public/ par un appelant sans identifiants : ce préfixe constitue la déclaration explicite que tout le monde peut la lire, y compris les visiteurs anonymes et les CDNs. La lecture de cette clé par un utilisateur connecté passe toujours par le hook, mais celle d’un utilisateur anonyme ne l’est pas, de sorte qu’un hook ne peut pas rendre privée une clé sous public/ ; stockez plutôt le fichier en dehors de public/. Seules ces deux lectures le contournent : un utilisateur que votre adaptateur d’authentification appellerait par hasard public est évalué comme n’importe qui d’autre.
  • Un bucket, un seul nom. Sur S3 et GCS, le bucket du hook est toujours "default", même lorsqu’une requête spécifie le bucket du fournisseur, qui cible les mêmes objets.
  • Un chemin est public lorsque la clé qu’il désigne l’est. Le préfixe public/ est évalué sur la clé canonique servie par la route — après suppression du segment initial de bucket default/ et résolution de //, ./ et du / initial — jamais sur l’URL telle qu’elle est écrite. /api/storage/file/notes://public/x.txt désigne la clé privée notes:/public/x.txt, et la clé default/public/x.txt (atteinte via /api/storage/file/default/default/public/x.txt) se trouve dans un dossier nommé default ; aucune des deux n’est publique. Dans un hook, testez la key qui vous est fournie avec isPublicStorageKey issu de @rebasepro/types — isPublicStoragePath lit un chemin ou une URL côté client, et les évalue tous deux comme publics.
  • list est contrôlé sur le préfixe. Le listing est le moyen de découvrir des clés dont personne ne vous a parlé.
  • Les téléversements avec reprise (TUS) sont contrôlés au moment de la création, évitant ainsi qu’un téléversement refusé ne laisse un fichier temporaire derrière lui.
  • Omettre le hook préserve le comportement précédent, les applications mono-locataires ne sont donc pas affectées.

Une personne connectée dispose de toutes les portées de stockage (scopes), le hook et les politiques ci-dessus décident donc pour elle. Une clé d’API a également besoin de la portée de stockage adéquate pour l’opération, sur la source désignée par la requête : storage:read pour les téléchargements, métadonnées et listings, storage:write pour les téléversements, dossiers et chaque étape d’un téléversement avec reprise, storage:delete pour les suppressions. La cible est l’identifiant de la source, et celui de la source par défaut est (default) : storage:read:(default), storage:write:avatars. Sans cela, la réponse est 403 SCOPE_MISSING. Avec la portée requise, le hook s’exécute tout de même, en adoptant l’identité représentée par la clé.

Toute autre écriture dans Rebase peut déclencher une réaction — une ligne dispose de beforeSave et afterSave, une planification dispose d’une tâche cron — alors qu’un téléversement ne disposait de rien. Tout ce qu’un téléversement impliquait devait être géré par le client, lors d’un second appel, ce qui signifie que l’action n’était pas réalisée du tout si le client se déconnectait entre-temps.

storageTriggers: [
{
path: "uploads/:uid/**",
events: ["finalize"],
handler: async ({ key, params, size, user }) => {
await indexUpload.enqueue({ key, uid: params.uid, size }); // a `queue()` in config/resources.ts
}
}
]

La syntaxe des modèles est la même que celle de storagePolicies — segments littéraux, * pour un segment, :name pour en capturer un, ** pour le reste — et un modèle mal formé fait échouer le démarrage plutôt que d’ignorer silencieusement les événements sans correspondance.

Événement Moment de déclenchement
finalize après que l’objet est écrit de manière durable ; jamais pour une écriture ayant échoué
delete après la suppression effective de l’objet

finalize se déclenche pour les téléversements multipart comme pour ceux avec reprise (TUS) — une seule fois par téléversement, et non par fragment — et un téléversement avec reprise indique l’utilisateur qui l’a créé, puisque c’est cette identité que l’autorisation a validée.

Ce qu’un gestionnaire (handler) ne doit pas présumer :

  • Un gestionnaire qui lève une exception ne fait pas échouer la requête. L’objet est déjà stocké au moment de son exécution ; renvoyer une erreur au client indiquerait que le téléversement a échoué alors que ce n’est pas le cas, et les clients réessayent leurs téléversements. Les échecs sont consignés dans les logs et la réponse reste inchangée. Si le traitement doit impérativement avoir lieu, ajoutez une tâche dans une file d’attente (queue).
  • Les gestionnaires sont attendus avec await, dans l’ordre de leur déclaration, avant l’envoi de la réponse — exécuter en tâche de fond (« fire and forget ») laisserait une promesse qu’un environnement serverless est libre de geler en plein vol. Un gestionnaire lent équivaut donc à un téléversement ralenti, ce qui constitue une autre raison d’utiliser une file d’attente plutôt que d’effectuer le travail ici.
  • Les écritures internes ne déclenchent pas de triggers. Le cache de rendu d’images écrit les objets dérivés directement dans le contrôleur de stockage ; un trigger ** réagissant à ces écritures se déclencherait sur sa propre sortie.