backend
Les fonctionnalités Postgres sur lesquelles je m'appuie vraiment
Les fonctionnalités Postgres sur lesquelles je m'appuie vraiment
Le domaine est intrinsèquement relationnel — les organisations ont des succursales, les succursales ont des programmes, les programmes ont des candidatures, les candidatures ont des documents, des paiements et un historique de statut. Postgres me donne une forte intégrité relationnelle pour ça, plus des colonnes JSON natives pour les parties vraiment flexibles — paramètres, configs de champs de formulaire — sans faire tourner une base documentaire à côté. Une seule base, pas deux, pour quelque chose que maintient une personne.
Tables sous un schéma core personnalisé, pas le public par défaut. Le namespacing compte dès que d'autres outils entrent en scène — poolers, zones logiques grandissantes, à terme plusieurs applications sur une instance. C'est directement lié au pire piège de pgBouncer : search_path est au niveau session, et un pooler qui réutilise les connexions peut en distribuer une avec le mauvais path réglé, pointant silencieusement les requêtes vers le mauvais schéma.
Enums natifs pour les champs à vocabulaire fixe :
CREATE TYPE application_status_enum AS ENUM
('draft','submitted','under_review','accepted','enrollment_pending','enrolled','rejected','withdrawn');
Imposé au niveau base — une valeur invalide ne peut être insérée depuis aucune couche — plutôt que de faire confiance au code applicatif pour valider une colonne de chaîne. Le coût est une migration pour ajouter une valeur, ce qui est un prix juste pour la garantie.
jsonb pour les données à forme vraiment variable — configs de formulaire, instantanés avant/après d'audit, métadonnées par entité — là où Postgres peut quand même indexer et requêter dans la structure, contrairement à une colonne de texte contenant du JSON sérialisé.
Index composites alignés sur les vraies requêtes, pas un index réflexe sur chaque clé étrangère :
@@index([tenant_id, status, created_at(sort: Desc)], map: "idx_app_tenant_status_created")
Ça sert « les candidatures de cette organisation, filtrées par statut, les plus récentes d'abord » — la requête d'admin la plus courante — en un seul parcours d'index au lieu de filtrer-puis-trier.
Index deleted_at sur chaque table à suppression douce, parce que les requêtes « non supprimé » filtrent sur deleted_at IS NULL constamment et ça doit rester rapide à l'échelle.
Index uniques partiels pour les champs qui sont optionnels mais doivent être uniques quand présents :
@@unique(map: "uq_applications_number", where: raw("(application_number IS NOT NULL)"))
Une contrainte unique simple a une sémantique propre à la base autour de plusieurs NULL ; l'index partiel rend l'intention réelle explicite.