8 min

Stockage d'objets vs blobs en base pour des uploads rapides et peu coûteux

Stockage d'objets vs blobs en base : modélisez les métadonnées dans Postgres, stockez les octets en stockage d'objets et gardez des téléchargements rapides avec des coûts prévisibles.

Stockage d'objets vs blobs en base pour des uploads rapides et peu coûteux

Le vrai problème des uploads utilisateurs

Les uploads utilisateurs paraissent simples : accepter un fichier, le sauvegarder, l'afficher plus tard. Ça fonctionne avec quelques utilisateurs et de petits fichiers. Puis le volume augmente, les fichiers grossissent, et la douleur apparaît là où on ne l'attend pas forcément, loin du bouton d'upload.

Les téléchargements ralentissent parce que votre serveur d'appli ou votre base de données effectue le travail lourd. Les sauvegardes deviennent énormes et lentes, donc les restaurations prennent plus de temps exactement quand vous en avez besoin. Les factures de stockage et de bande passante (egress) peuvent exploser parce que les fichiers sont servis de façon inefficace, dupliqués ou jamais nettoyés.

Ce que vous voulez en général, c'est ennuyeusement fiable : des transferts rapides sous charge, des règles d'accès claires, des opérations simples (sauvegarde, restauration, nettoyage) et des coûts qui restent prévisibles quand l'usage croît.

Pour y arriver, séparez deux choses qu'on mélange souvent :

Les métadonnées sont de petites informations sur un fichier : qui en est propriétaire, comment il s'appelle, la taille, le type, quand il a été uploadé, et où il se trouve. Elles appartiennent à votre base de données (comme Postgres) parce que vous devez les interroger, filtrer et joindre aux utilisateurs, projets et permissions.

Les octets du fichier sont le contenu réel (la photo, le PDF, la vidéo). Stocker les octets dans des blobs de la base peut fonctionner, mais alourdit la base, augmente les sauvegardes et rend les performances moins prévisibles. Mettre les octets dans du stockage d'objets garde la base concentrée sur ce qu'elle fait de mieux, tandis que les fichiers sont servis rapidement et à moindre coût par des systèmes conçus pour ça.

Stockage d'objets vs blobs en base, en termes simples

Quand on dit «stocker les uploads dans la base», on pense généralement aux blobs : soit une colonne BYTEA (octets bruts dans une ligne) soit les «large objects» de Postgres (fonctionnalité qui stocke les grandes valeurs séparément). Les deux peuvent fonctionner, mais confient à la base le rôle de servir les octets.

Le stockage d'objets est une autre idée : le fichier vit dans un bucket comme objet, adressé par une clé (par exemple uploads/2026/01/file.pdf). Il est conçu pour les gros fichiers, le stockage peu coûteux et le streaming. Il gère aussi de nombreuses lectures concurrentes sans monopoliser vos connexions DB.

Postgres excelle pour les requêtes, contraintes et transactions. Il est parfait pour les métadonnées : qui possède le fichier, de quoi il s'agit, quand il a été uploadé et s'il peut être téléchargé. Ces métadonnées sont petites, faciles à indexer et à garder cohérentes.

Règle pratique :

  • Utilisez Postgres pour les métadonnées, permissions et relations.
  • Utilisez le stockage d'objets pour les octets quand les fichiers dépassent quelques Mo, ou quand les téléchargements sont fréquents.
  • Envisagez les blobs DB seulement pour de petits éléments qui doivent être attachés transactionnellement à un enregistrement (comme une petite icône) et si vous êtes sûr que la croissance DB restera modeste.

Un contrôle de bon sens : si les sauvegardes, réplicas et migrations deviennent douloureux avec les octets inclus, laissez les octets en dehors de Postgres.

Une architecture simple qui reste maîtrisable

Le montage que la plupart des équipes adoptent est simple : stockez les octets dans un stockage d'objets, et conservez l'enregistrement de fichier (qui en est propriétaire, ce que c'est, où il se trouve) dans Postgres. Votre API coordonne et autorise, mais ne fait pas transiter les gros uploads et téléchargements.

Cela vous donne trois responsabilités claires :

  • Postgres contient une petite ligne par fichier : un file_id stable, propriétaire, taille, content type et le pointeur vers l'objet.
  • Le stockage d'objets contient les octets réels, optimisés pour les gros fichiers et le stockage économique.
  • Votre API crée et autorise les enregistrements de fichier, et délivre des permissions de courte durée au stockage.

Ce file_id stable devient la clé primaire pour tout : commentaires qui référencent une pièce jointe, factures qui pointent vers un PDF, logs d'audit et outils de support. Les utilisateurs peuvent renommer un fichier, vous pouvez le déplacer entre buckets, et le file_id reste inchangé.

Quand c'est possible, traitez les objets stockés comme immuables. Si un utilisateur remplace un document, créez un nouvel objet (et généralement une nouvelle ligne ou une nouvelle ligne de version) au lieu d'écraser les octets en place. Ça simplifie le caching, évite les surprises du type «l'ancien lien retourne un nouveau fichier» et donne une histoire de rollback propre.

Décidez de la confidentialité tôt : privé par défaut, public seulement par exception. Bonne règle : la base de données est la source de vérité pour qui peut accéder, le stockage d'objets applique la permission de courte durée que votre API délivre.

Comment modéliser les métadonnées de fichier dans Postgres

Avec la séparation propre, Postgres stocke les faits sur le fichier et le stockage d'objets stocke les octets. Cela garde votre base plus petite, les sauvegardes plus rapides et les requêtes simples.

Une table uploads pratique n'a besoin que de quelques champs pour répondre à des questions réelles comme «qui possède ça ?», «où est-ce stocké ?» et «est-ce sûr à télécharger ?»

CREATE TABLE uploads (
  id               uuid PRIMARY KEY,
  owner_id         uuid NOT NULL,
  bucket           text NOT NULL,
  object_key       text NOT NULL,
  size_bytes       bigint NOT NULL,
  content_type     text,
  original_filename text,
  checksum         text,
  state            text NOT NULL CHECK (state IN ('pending','uploaded','failed','deleted')),
  created_at       timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX uploads_owner_created_idx ON uploads (owner_id, created_at DESC);
CREATE INDEX uploads_checksum_idx ON uploads (checksum);

Quelques décisions qui évitent des soucis plus tard :

  • Utilisez bucket + object_key comme pointeur de stockage. Gardez-le immuable une fois uploadé.
  • Suivez l'état. Quand un utilisateur commence un upload, insérez une ligne pending. Basculez en uploaded seulement après que votre système confirme que l'objet existe et que la taille (et idéalement le checksum) correspond.
  • Stockez original_filename pour l'affichage seulement. Ne vous fiez pas à ce champ pour des décisions de type ou de sécurité.

Si vous supportez les remplacements (comme un utilisateur qui ré-uploade une facture), ajoutez une table upload_versions séparée avec upload_id, version, object_key et created_at. Ainsi vous conservez l'historique, pouvez revenir en arrière et évitez de casser d'anciennes références.

Flux d'upload étape par étape (sans bloquer votre API)

Garde les uploads rapides en faisant gérer à votre API la coordination, pas les octets. Votre base reste réactive, tandis que le stockage d'objets prend le hit de bande passante.

Commencez par créer un enregistrement d'upload avant l'envoi. Votre API renvoie un upload_id, où le fichier sera stocké (un object_key) et une permission d'upload de courte durée.

Un flux courant :

  1. Le client demande à uploader : votre API crée une ligne pending, plus la taille attendue et le content type prévu.
  2. L'API renvoie une URL présignée : pour les gros fichiers, générez une URL d'upload présignée. Pour les petits fichiers (avatars), vous pouvez encore proxyfier via votre backend si vous préférez simplifier le code client.
  3. Le client upload directement vers le stockage d'objets : le navigateur ou l'app mobile envoie les octets au stockage, pas via votre API.
  4. Finaliser : le client appelle votre API avec upload_id et éventuellement des champs de réponse du stockage (comme ETag). Votre serveur vérifie la taille, le checksum (si vous en utilisez un) et le content type, puis marque la ligne uploaded.
  5. Échouer proprement : si la vérification échoue, marquez failed et supprimez éventuellement l'objet.

Les retries et doublons sont normaux. Faites de l'appel de finalisation une opération idempotente : si le même upload_id est finalisé deux fois, retournez le succès sans rien altérer.

Pour réduire les doublons dus aux retries et ré-uploads, stockez un checksum et considérez «même propriétaire + même checksum + même taille» comme le même fichier.

Flux de téléchargement étape par étape (rapide et cache-friendly)

Garder votre stack portable
Exportez le code source quand vous voulez garder le contrôle total sur fournisseurs et politiques de stockage.

Un bon flux de téléchargement commence par une URL stable dans votre application, même si les octets vivent ailleurs. Pensez : /files/{file_id}. Votre API utilise file_id pour lire les métadonnées dans Postgres, vérifie la permission, puis décide comment livrer le fichier.

  1. Le client demande votre URL stable avec file_id.
  2. L'API vérifie que l'utilisateur peut y accéder et que le fichier est uploaded.
  3. L'API retourne soit une redirection vers le stockage d'objets (souvent préférable), soit une URL GET présignée de courte durée pour les fichiers privés.
  4. Le client télécharge directement depuis le stockage d'objets, gardant votre API et serveurs hors du chemin critique.

Les redirections sont simples et rapides pour des fichiers publics ou semi-publics. Pour les fichiers privés, les URLs GET présignées gardent le stockage privé tout en permettant au navigateur de télécharger directement.

Pour la vidéo et les gros téléchargements, assurez-vous que le stockage d'objets (et toute couche proxy) supporte les requêtes de plage (Range headers). Cela permet la recherche et les téléchargements reprenables. Si vous faites transiter les octets par votre API, le support des ranges casse souvent ou devient coûteux.

Le caching est là où la vitesse apparaît. Votre endpoint stable /files/{file_id} doit généralement rester non-cacheable (c'est la porte d'autorisation), tandis que la réponse du stockage d'objets peut souvent être mise en cache selon le contenu. Si les fichiers sont immuables (nouvel upload = nouvelle clé), vous pouvez définir un long TTL. Si vous écrasez des fichiers, gardez les temps de cache courts ou utilisez des clés versionnées.

Un CDN aide quand vous avez beaucoup d'utilisateurs globaux ou de gros fichiers. Si votre audience est réduite ou surtout dans une région, le stockage d'objets seul suffit souvent et coûte moins cher pour commencer.

Garder les coûts prévisibles sur la durée

Les factures surprises viennent généralement des téléchargements et du churn, pas des octets bruts sur le disque.

Évaluez quatre leviers qui font bouger la facture : combien vous stockez, la fréquence des lectures et écritures (requêtes), la quantité de données qui quittent le fournisseur (egress) et si vous utilisez un CDN pour réduire les téléchargements répétés depuis l'origine. Un petit fichier téléchargé 10 000 fois peut coûter plus qu'un gros fichier que personne ne touche.

Des contrôles pour stabiliser les dépenses :

  • Limiter la taille des fichiers par upload et définir des quotas par utilisateur selon le plan.
  • Limiter le débit d'uploads et de downloads pour éviter abus et boucles accidentelles.
  • Utiliser des règles de cycle de vie pour que les vieux fichiers passent sur un stockage moins cher ou expirent quand ils ne sont plus nécessaires.
  • Dédupliquer par checksum pour que les retries ou ré-uploads n'engendrent pas des copies supplémentaires.
  • Stocker des compteurs d'usage dans Postgres pour que la facturation et les alertes se basent sur des faits.

Les règles de cycle de vie sont souvent le gain le plus simple. Par exemple : garder les photos originales «hot» pendant 30 jours puis les déplacer vers une classe moins chère ; garder les factures pendant 7 ans ; supprimer les parties d'upload échouées après 7 jours. Même des politiques de rétention basiques stoppent la dérive du stockage.

La déduplication peut être simple : stockez un hash de contenu (par exemple SHA-256) dans la table métadonnées et appliquez l'unicité par propriétaire. Quand un utilisateur upload le même PDF deux fois, vous pouvez réutiliser l'objet existant et créer juste une nouvelle ligne métadonnée.

Enfin, suivez l'utilisation là où vous faites déjà la comptabilité utilisateur : Postgres. Stockez bytes_uploaded, bytes_downloaded, object_count et last_activity_at par utilisateur ou workspace. Cela permet d'afficher des limites dans l'UI et de déclencher des alertes avant la facture.

Sécurité et conformité basiques pour les uploads

Mettre en place des uploads présignés
Créez des uploads directs vers le stockage avec une étape de finalisation, sans proxy des octets.

La sécurité des uploads se résume à deux choses : qui peut accéder à un fichier, et ce que vous pouvez prouver plus tard si quelque chose tourne mal.

Contrôle d'accès qui reflète l'usage réel

Commencez par un modèle d'accès clair et encodez-le dans Postgres, pas dans des règles ponctuelles dispersées entre services.

Un modèle simple couvre la plupart des apps :

  • Owner-only : seul l'uploader (et les admins) peuvent accéder.
  • Shared : accessible à des utilisateurs spécifiques ou à une équipe/workspace.
  • Public : accessible sans authentification (à utiliser avec parcimonie, et toujours tracer).

Pour les fichiers privés, évitez d'exposer des clés d'objet brutes. Délivrez des URLs d'upload et de téléchargement à durée limitée et à portée limitée, et faites-les tourner souvent.

Contrôles de conformité qui vous sauvent après-coup

Vérifiez le chiffrement en transit et au repos. En transit signifie HTTPS end-to-end, y compris pour les uploads directs vers le stockage. Au repos signifie chiffrement côté fournisseur de stockage, et que les sauvegardes et réplicas sont aussi chiffrés.

Ajoutez des points de contrôle pour la sécurité et la qualité des données : validez le content type et la taille avant de délivrer une URL d'upload, puis validez à nouveau après l'upload (sur la base des octets réellement stockés, pas seulement du nom de fichier). Si votre profil de risque l'exige, exécutez un scan antivirus asynchrone et mettez le fichier en quarantaine jusqu'à passage.

Conservez des champs d'audit pour pouvoir enquêter : uploaded_by, ip, user_agent et last_accessed_at constituent une base pratique.

Si vous avez des exigences de localisation des données, choisissez la région de stockage délibérément et gardez-la cohérente avec où vous exécutez le calcul.

Erreurs courantes qui causent ralentissements et incidents

La plupart des problèmes d'upload ne concernent pas la vitesse brute. Ils viennent de choix de conception pratiques au début puis devenus pénibles quand le trafic, les données et le support arrivent.

  • Stocker les octets de fichier dans Postgres : ça marche pour les petites apps, puis les sauvegardes explosive, les restaurations prennent des heures et la maintenance courante devient risquée. Une grosse table peut ralentir vacuum, réplication et même des requêtes simples.
  • Utiliser le nom de fichier utilisateur comme clé d'objet : les collisions arrivent (deux utilisateurs uploadent "invoice.pdf") et des caractères étranges créent des cas limites. Gardez les noms de fichier pour l'affichage, mais générez une clé unique (genre UUID) pour le stockage.
  • Sauter la validation au moment de la finalisation : même si vous validez côté client, vous devez vérifier côté serveur la taille, le content type et la propriété avant de marquer l'upload comme terminé.
  • Rendre des objets publics par erreur et ne jamais faire tourner l'accès : une politique de bucket «temporaire» publique ou des URLs longue durée deviennent souvent permanentes. Préférez des liens de courte durée et un moyen de révoquer l'accès rapidement.
  • Supprimer un seul côté (métadonnées ou octets) : supprimer la ligne Postgres mais laisser l'objet crée une fuite de coûts silencieuse. Supprimer l'objet mais garder la métadonnée crée des téléchargements cassés et une surcharge support.

Un exemple concret : si un utilisateur remplace sa photo de profil trois fois, vous pouvez vous retrouver à payer pour trois anciens objets à moins de planifier un nettoyage. Un pattern sûr est une suppression logique (soft delete) dans Postgres, puis un job en background qui supprime l'objet et enregistre le résultat.

Checklist rapide avant le lancement

La plupart des problèmes apparaissent quand arrive le premier gros fichier, qu'un utilisateur rafraîchit en plein upload, ou que quelqu'un supprime un compte et que les octets restent derrière.

Assurez-vous que votre table Postgres enregistre la taille du fichier, le checksum (pour vérifier l'intégrité) et un chemin d'état clair (par exemple : pending, uploaded, failed, deleted).

Une checklist de dernier kilomètre :

  • Vérifiez que les retries sont sûrs : les tentatives répétées ne doivent pas créer d'objets supplémentaires ni de lignes "uploaded" sans octets.
  • Rendez les uploads résumables ou au moins relançables sans tickets de support (les timeouts et réseaux mobiles arriveront).
  • Vérifiez que les téléchargements gèrent les requêtes de plage pour que les gros fichiers démarrent vite et reprennent après une pause.
  • Définissez la suppression de bout en bout : marquer la métadonnée, supprimer les octets et gérer le nettoyage différé si un job échoue.
  • Ajoutez une surveillance basique : taux d'erreur upload/download, croissance du stockage et pics d'egress soudains.

Un test concret : uploadez un fichier de 2 Go, rafraîchissez la page à 30%, puis reprenez. Puis téléchargez-le sur une connexion lente et allez au milieu. Si l'un des flux est bancal, corrigez-le maintenant, pas après le lancement.

Scénario d'exemple : photos et factures dans une même app

Modéliser les métadonnées de fichier rapidement
Générez des tables de métadonnées, états et index pour que les uploads restent interrogeables et sûrs.

Une SaaS simple a souvent deux types d'uploads très différents : photos de profil (fréquentes, petites, sûres à mettre en cache) et factures PDF (sensibles, privées). C'est là que la séparation métadonnées/Postgres et octets/stockage d'objets paie.

Voici à quoi peuvent ressembler les métadonnées dans une table files, avec quelques champs qui influencent le comportement :

fieldexemple photo profilexemple PDF facture
kindavatarinvoice_pdf
visibilityprivate (servi via URL signée)private
cache_controlpublic, max-age=31536000, immutableno-store
object_keyusers/42/avatars/2026-01-17T120102Z.webporgs/7/invoices/INV-1049.pdf
statusuploadeduploaded
size_bytes184233982341

Quand un utilisateur remplace une photo, traitez-la comme un nouveau fichier, pas comme un écrasement. Créez une nouvelle ligne et un nouveau object_key, puis mettez à jour le profil utilisateur pour pointer vers le nouvel file_id. Marquez l'ancienne ligne replaced_by=<new_id> (ou deleted_at) et supprimez l'ancien objet plus tard avec un job en background. Cela conserve l'historique, facilite les rollbacks et évite les conditions de course.

Le support et le debug deviennent plus faciles parce que les métadonnées racontent une histoire. Quand quelqu'un dit «mon upload a échoué», le support peut vérifier status, un last_error lisible, un storage_request_id ou etag (pour tracer les logs du stockage), des timestamps (est-ce que ça a stallé ?) et owner_id et kind (la politique d'accès est-elle correcte ?).

Prochaines étapes pour implémenter sans surconstruire

Commencez petit et rendez le chemin heureux ennuyeusement fiable : les fichiers s'uploadent, les métadonnées se sauvegardent, les téléchargements sont rapides et rien ne se perd.

Une bonne première étape est une table Postgres minimale pour les métadonnées + un flux d'upload direct vers le stockage et un flux de téléchargement unique que vous pouvez expliquer sur un tableau blanc. Une fois que cela marche de bout en bout, ajoutez versions, quotas et règles de cycle de vie.

Définissez une politique de stockage claire par type de fichier et notez-la. Par exemple, les photos de profil sont cacheables, tandis que les factures sont privées et n'accessibles que via des URLs de téléchargement de courte durée. Mélanger les politiques dans un même préfixe de bucket sans plan est la source d'expositions accidentelles.

Ajoutez de l'instrumentation tôt. Les métriques à vouloir dès le jour 1 : taux d'échec de finalisation d'upload, taux d'orphelins (objets sans ligne DB correspondante et vice versa), volume d'egress par type de fichier, latence P95 des téléchargements et taille moyenne des objets.

Si vous voulez prototyper plus vite, Koder.ai (koder.ai) est construit pour générer des apps complètes depuis un chat, et il colle à la stack commune utilisée ici (React, Go, Postgres). Cela peut être un moyen pratique d'itérer sur le schéma, les endpoints et les jobs de nettoyage sans réécrire sans cesse le même squelette.

Après ça, n'ajoutez que ce que vous pouvez expliquer en une phrase : «on garde les anciennes versions 30 jours» ou «chaque workspace a 10 Go». Restez simple jusqu'à ce que l'usage réel vous force à changer.

FAQ

Dois-je stocker les fichiers uploadés dans Postgres ou dans du stockage d'objets ?

Utilisez Postgres pour les métadonnées que vous devez interroger et sécuriser (propriétaire, permissions, état, checksum, pointeur). Placez les octets dans un stockage d'objets pour que les téléchargements et transferts volumineux n'utilisent pas les connexions de la base de données ni n'alourdissent les sauvegardes.

Quel est le principal inconvénient de stocker les octets de fichier dans des blobs Postgres ?

Cela fait de votre base de données un serveur de fichiers. La taille des tables augmente, les sauvegardes et restaurations ralentissent, la réplication se charge davantage, et les performances deviennent moins prévisibles quand beaucoup d'utilisateurs téléchargent en même temps.

Quelle est l'architecture d'upload la plus simple qui scale sans se compliquer ?

Oui. Gardez un file_id stable dans votre application, stockez les métadonnées dans Postgres et les octets dans un stockage d'objets adressés par bucket et object_key. Votre API doit autoriser l'accès et délivrer des permissions d'upload/téléchargement de courte durée plutôt que de proxyfier les octets.

Comment doit fonctionner le flux d'upload si je ne veux pas que mon API gère les octets ?

Créez d'abord une ligne pending, générez un object_key unique, puis laissez le client uploader directement vers le stockage avec une permission court terme. Après l'upload, le client appelle un endpoint de finalisation pour que le serveur vérifie la taille et le checksum (si utilisé) avant de marquer la ligne uploaded.

Pourquoi ai-je besoin d'un «état» d'upload comme pending/uploaded/failed/deleted ?

Parce que les uploads réels échouent et se réessaient. Un champ d'état permet de distinguer les fichiers attendus mais absents (pending), complétés (uploaded), corrompus (failed) et supprimés (deleted) afin que l'interface, les jobs de nettoyage et les outils de support fonctionnent correctement.

Puis-je utiliser le nom de fichier fourni par l'utilisateur comme clé d'objet dans le stockage ?

Considérez original_filename comme un champ d'affichage uniquement. Générez une clé de stockage unique (souvent une arborescence basée sur UUID) pour éviter les collisions, les caractères étranges et les surprises de sécurité. Vous pouvez toujours afficher le nom original dans l'UI tout en gardant les chemins de stockage propres et prévisibles.

Quel est le modèle de téléchargement recommandé pour les fichiers privés ?

Utilisez une URL applicative stable comme /files/{file_id} comme garde d'accès. Après vérification des droits dans Postgres, retournez une redirection ou une permission de téléchargement signée courte durée pour que le client télécharge directement depuis le stockage d'objets, en gardant votre API hors du chemin critique.

Qu'est-ce qui cause généralement des coûts surprises avec les uploads utilisateurs ?

L'egress et les téléchargements répétés dominent souvent la facture, pas le stockage brut. Limitez la taille des fichiers et appliquez des quotas, utilisez des règles de rétention, dédupliquez par checksum quand c'est pertinent, et suivez les compteurs d'usage pour pouvoir alerter avant l'explosion des coûts.

Quelles vérifications de sécurité valent la peine dès le jour un pour les uploads ?

Stockez les permissions et la visibilité dans Postgres comme source de vérité, et gardez le stockage privé par défaut. Validez le type et la taille avant et après l'upload, utilisez HTTPS de bout en bout, chiffrez au repos, et conservez des champs d'audit pour pouvoir enquêter plus tard.

Comment implémenter cela rapidement sans surconstruire ?

Commencez avec une table de métadonnées, un flux d'upload direct vers le stockage et un endpoint de téléchargement borné, puis ajoutez des jobs de nettoyage pour les objets orphelins et les lignes supprimées logiquement. Si vous voulez prototyper vite sur une stack React/Go/Postgres, Koder.ai peut générer les endpoints, le schéma et les tâches d'arrière-plan depuis un chat pour vous faire gagner du temps.

Related posts