Sécurité
Chaque endpoint déclare qui peut l'appeler, chaque espace de travail n'atteint que ses propres données, et jetons et webhooks sont vérifiés avant tout. Limites de débit, HTTPS, sauvegardes nocturnes et alertes sont prêts pour la production.
En bref
- Un endpoint sans ses guards fait échouer le build.
- Un client ne voit jamais les données d'un autre, et les tests le prouvent.
- Prêt pour la production : une checklist suffit pour passer en ligne.
Chaque endpoint dit qui peut l'appeler
Chaque query, mutation et route déclare un guard d'authentification et un guard de permission. « Public » et « aucune permission requise » s'écrivent aussi en toutes lettres, pour ressortir en revue. Un guard manquant est une erreur de lint.
- La connexion est vérifiée une fois, dans un middleware ; les resolvers ne lisent jamais de jeton
- Déconnecté donne UNAUTHENTICATED, une permission manquante donne FORBIDDEN
@Resolver(() => ProjectDto)@UseGuards(WorkspaceAuthGuard)@UseFilters(ProjectGraphqlApiExceptionFilter)export class ProjectResolver { constructor(private readonly projectService: ProjectService) {} @Query(() => [ProjectDto]) @UseGuards(NoPermissionGuard) async projects( @Mutation(() => ProjectDto) @UseGuards(WorkspacePermissionGuard('org:projects:create')) async createProject(Les espaces de travail ne se croisent pas
Les données d'un espace de travail ne sont lues et écrites que via un repository qui ajoute l'espace de travail à chaque requête. Il refuse un espace de travail écrit dans la requête, une ligne déplacée vers un autre espace, et un upsert qui pourrait tomber sur la ligne d'un autre espace.
- L'espace de travail vient toujours du membre connecté, jamais de la requête
- Une règle de lint refuse tout autre accès aux tables d'un espace de travail
- Les tests de l'API vérifient qu'un autre espace de travail obtient « introuvable » pour chaque action
// The only way services read or write tenant-owned data. Every method takes// the workspaceId first and adds it to the query itself, so forgetting// `where: { workspaceId }` is impossible and a caller can't override it.export class WorkspaceScopedRepository<TEntity extends WorkspaceRelatedEntity> { constructor(private readonly repository: Repository<TEntity>) {} async find( workspaceId: string, options: FindManyOptions<TEntity> = {}, ): Promise<TEntity[]> { return this.repository.find({ ...options, where: this.mergeWorkspaceIdIntoWhere(workspaceId, options.where), }); }Les jetons sont vérifiés, pas crus sur parole
Clerk détient les mots de passe, les sessions et la validation en deux étapes : votre base ne stocke jamais d'identifiants de connexion. Le serveur vérifie chaque jeton de session avec l'outil officiel de Clerk et la clé publique de votre instance, sans appel réseau.
- Noms et e-mails sont lus chez Clerk par le serveur, jamais repris du navigateur
- Les permissions découlent du rôle du membre, dans votre code
Authorization: Bearer eyJhbGciOiJSUzI1NiIs…Ce que le serveur vérifie avant tout
- Signé avec la clé de votre instance Clerk, vérifié sans appel réseau
- Émis pour l'adresse de votre app web
- Encore valide : les jetons vivent 60 secondes
- Les identifiants Clerk remplacés par les vôtres
Les webhooks sont vérifiés avant tout
Stripe et Clerk signent leurs webhooks. Le serveur vérifie la signature sur les octets exacts reçus, puis enregistre l'événement une seule fois dans une table de réception : un événement rejoué n'est traité qu'une fois.
- Un forfait ne change que sur un webhook signé de Stripe
- Votre base ne stocke ni carte, ni adresse, ni numéro fiscal, ni facture
- Un cron reprend tout événement laissé en route, et vous alerte si l'un d'eux échoue sans cesse
| Requête | Réponse | Ce qui se passe |
|---|---|---|
| Signature valide | 200 | Enregistré une fois, traité par le worker |
| Le même événement, à nouveau | 200 | Pas traité deux fois |
| Signature absente ou fausse | 400 | Rien n'est enregistré |
| Fournisseur désactivé | 404 | Rien n'est enregistré |
Les abus sont plafonnés avant de vous coûter
Le débit des actions sensibles est limité, avec des compteurs dans Redis : un script ou un bouton bloqué ne peut pas submerger Stripe, Clerk ou votre base de données. Les requêtes GraphQL démesurées sont refusées avant toute exécution, et en production l'API ne décrit son schéma à personne.
- Au-delà d'une limite, on lit « Trop de tentatives », dans sa langue ; les expéditeurs de webhooks reçoivent un 429 et reviennent plus tard
- La protection anti-bots de Clerk vérifie chaque inscription
| Action | Compté par | Autorisé |
|---|---|---|
| Ouvrir Stripe Checkout ou le portail de facturation | Espace de travail | 10 par minute chacune |
| Renommer l'espace de travail | Espace de travail | 10 par minute |
| Créer un projet | Personne | 30 par minute |
| Webhooks Stripe et Clerk | Adresse IP | 600 par minute chacun |
Une production renforcée et surveillée
Tout le produit tourne sur un serveur avec Docker. Seul le proxy est joignable : il obtient lui-même les certificats HTTPS et envoie des en-têtes de sécurité stricts. Les migrations tournent à chaque déploiement, la base de données est sauvegardée chaque nuit, et vous êtes alerté quand quelque chose casse.
- Les conteneurs de l'app tournent sans root, sur un système de fichiers en lecture seule
- Logs JSON avec un identifiant de requête ; alertes par e-mail via Sentry, litiges de paiement compris
- Les secrets que vos fonctionnalités stockent sont chiffrés en AES-256-GCM
edgeHTTPS et en-têtes de sécuritéEn marchemigrateMigrations, puis s'arrêteTerminéapiAPI, au débit limitéOpérationnelworkerTâches et cronsEn marchebackupChaque nuit, 7 conservéesOpérationnelpostgresAucun port publicOpérationnelInstallations renforcées, secrets hors de git
Yarn ignore les scripts d'installation des paquets téléchargés, le moyen de propagation le plus courant des logiciels malveillants npm, et refuse les versions de moins de 3 jours. Le serveur valide sa configuration au démarrage, et les secrets ne vivent que dans des fichiers ignorés par git.
- En production, le serveur refuse de démarrer avec la connexion de remplacement, sans limites de débit ou sans sa clé de chiffrement
- Les erreurs inattendues arrivent au navigateur en « Internal Server Error », sans détails
- La CI installe depuis un lockfile qui ne peut pas changer pendant son exécution
enableConstraintsChecks: true enableHardenedMode: true enableScripts: false nodeLinker: node-modules npmMinimalAgeGate: 3dQuestions
Est-il prêt pour la production ?
Oui. Limites de débit, limites de requêtes, en-têtes de sécurité, HTTPS automatique, sauvegardes nocturnes, logs et alertes sont intégrés. Suivez le guide de déploiement, puis la checklist qui clôt le guide de sécurité : clés de production, domaines, instance de production Clerk et sa protection anti-bots, Stripe en mode live, sauvegardes copiées hors du serveur.
Où sont stockées les données de paiement ?
Chez Stripe. Votre base garde l'état de l'abonnement, jamais les cartes, les adresses, les numéros fiscaux ni les factures.
Un assistant IA peut-il modifier la production ?
Les règles disent non : les assistants ont un accès en lecture seule à la base, et la production ne change que par du code relu, livré par le déploiement normal.
Et si j'ajoute l'envoi de fichiers ou des e-mails ?
Ils ne sont pas inclus : ajoutez-les quand votre produit en a besoin. Les règles de sécurité disent déjà comment les protéger (fichiers sûrs, HTML d'e-mail nettoyé, adresses vérifiées), et votre assistant IA suit ces règles quand il construit la fonctionnalité.
Découvrir les autres fonctionnalités
- Connexion et espaces de travailComptes Clerk, équipes et rôles vérifiés dans votre code.
- FacturationAbonnements Stripe par espace de travail, pilotés par webhooks.
- Garde-fous pour l'IAUn règlement, des règles de lint qui expliquent, et yarn verify.
- Design system70 composants, 466 icônes et des tokens pour le clair et le sombre.
- ArchitectureUn seul dépôt : app web, API, worker et code partagé.
Démarrez votre prochaine app sur une base propre
Payez une fois et obtenez tout le code source, avec toutes les mises à jour futures.