auth : le fragment Prisma n'a pas la table rateLimit, que rateLimitStorage: 'database' exige #88

Closed
opened 2026-09-22 09:27:23 +00:00 by nicolas-fryder · 1 comment

Ce qui se passe

creerAuth({ rateLimitStorage: 'database' }) — que le commentaire de l'option recommande explicitement pour la production, « partagé entre répliques, survit au redémarrage » — fait écrire better-auth dans un modèle rateLimit.

@eqs/auth/schemas/auth.prisma déclare cinq modèles : User, Session, Account, Verification, ApiKey. Pas RateLimit.

Un satellite qui inclut le fragment tel quel et suit la recommandation obtient, sur chaque route gardée :

Prisma schema mismatch

  Missing tables
    rateLimit

  help: Run `npx auth generate` to refresh the Prisma schema, then run `prisma migrate`.

Pourquoi ça mérite un ticket plutôt qu'une ligne de documentation

Le symptôme est un 500 sur toutes les routes, pas sur la route de connexion : le throttle est consulté par la résolution du principal. Un service qui démarre normalement, qui répond sur /health, et qui refuse tout le monde derrière — pour une table absente. Le message de better-auth renvoie vers npx auth generate, qui n'a pas de sens dans un satellite dont le schéma vient du fragment.

C'est aussi un piège asymétrique : en développement, beaucoup laisseront rateLimitStorage à memory et ne verront rien. La panne se déclare au premier déploiement qui suit la recommandation.

Ce que ça donne

La forme, lue dans better-auth/dist/api/rate-limiter/index.mjs (v1.7.5) :

model RateLimit {
  key         String @id
  count       Int
  lastRequest BigInt @map("last_request")

  @@map("rate_limit")
}

lastRequest est un horodatage en millisecondes (Date.now()), pas une date. Un DateTime passerait la création et échouerait à la comparaison de fenêtre — donc un throttle qui ne throttle pas, ce qui est pire qu'un throttle absent puisqu'on croit l'avoir.

Suggestion

Ajouter le modèle au fragment. Si l'option doit rester à memory par défaut, le commentaire de rateLimitStorage gagnerait à dire que database suppose cette table — mais l'avoir dans le fragment coûte une table vide à ceux qui ne s'en servent pas, et évite la panne à tous les autres.

Contourné

ATLAS (rétrofit, étape 5) déclare le modèle lui-même et le crée dans sa migration 20240101000026_auth_socle, avec un commentaire pointant ce ticket. Rien à retirer quand le socle l'intégrera : le modèle sera simplement déjà là.

Versions : @eqs/auth@0.12.2, better-auth@1.7.5, Prisma 6.19, PostgreSQL 16.

## Ce qui se passe `creerAuth({ rateLimitStorage: 'database' })` — que le commentaire de l'option recommande explicitement pour la production, « partagé entre répliques, survit au redémarrage » — fait écrire better-auth dans un modèle `rateLimit`. `@eqs/auth/schemas/auth.prisma` déclare cinq modèles : `User`, `Session`, `Account`, `Verification`, `ApiKey`. Pas `RateLimit`. Un satellite qui inclut le fragment tel quel et suit la recommandation obtient, sur **chaque route gardée** : ``` Prisma schema mismatch Missing tables rateLimit help: Run `npx auth generate` to refresh the Prisma schema, then run `prisma migrate`. ``` ## Pourquoi ça mérite un ticket plutôt qu'une ligne de documentation Le symptôme est un **500 sur toutes les routes**, pas sur la route de connexion : le throttle est consulté par la résolution du principal. Un service qui démarre normalement, qui répond sur `/health`, et qui refuse tout le monde derrière — pour une table absente. Le message de better-auth renvoie vers `npx auth generate`, qui n'a pas de sens dans un satellite dont le schéma vient du fragment. C'est aussi un piège asymétrique : en développement, beaucoup laisseront `rateLimitStorage` à `memory` et ne verront rien. La panne se déclare au premier déploiement qui suit la recommandation. ## Ce que ça donne La forme, lue dans `better-auth/dist/api/rate-limiter/index.mjs` (v1.7.5) : ```prisma model RateLimit { key String @id count Int lastRequest BigInt @map("last_request") @@map("rate_limit") } ``` `lastRequest` est un horodatage en **millisecondes** (`Date.now()`), pas une date. Un `DateTime` passerait la création et échouerait à la comparaison de fenêtre — donc un throttle qui ne throttle pas, ce qui est pire qu'un throttle absent puisqu'on croit l'avoir. ## Suggestion Ajouter le modèle au fragment. Si l'option doit rester à `memory` par défaut, le commentaire de `rateLimitStorage` gagnerait à dire que `database` suppose cette table — mais l'avoir dans le fragment coûte une table vide à ceux qui ne s'en servent pas, et évite la panne à tous les autres. ## Contourné ATLAS (rétrofit, étape 5) déclare le modèle lui-même et le crée dans sa migration `20240101000026_auth_socle`, avec un commentaire pointant ce ticket. Rien à retirer quand le socle l'intégrera : le modèle sera simplement déjà là. Versions : `@eqs/auth@0.12.2`, `better-auth@1.7.5`, Prisma 6.19, PostgreSQL 16.
Author
Owner

Correction sur la forme : mon premier message donnait key en clé primaire. C'est faux, et l'erreur est instructive.

La définition réelle est dans @better-auth/core/src/db/get-tables.ts :

const rateLimitTable = {
  rateLimit: {
    modelName: options.rateLimit?.modelName || 'rateLimit',
    fields: {
      key:         { type: 'string', unique: true, required: true },
      count:       { type: 'number', required: true },
      lastRequest: { type: 'number', bigint: true, required: true, defaultValue: () => Date.now() },
    },
  },
}

key porte l'unicité, pas la clé primaire. better-auth donne un id à chaque modèle, implicitement, et vérifie sa présence au démarrage. Le fragment correct est donc :

model RateLimit {
  id          String @id
  key         String @unique
  count       Int
  lastRequest BigInt

  @@map("rate_limit")
}

Sans le id, le message change mais pas le symptôme :

Prisma schema mismatch

  Missing columns
    rateLimit.id

et le service refuse toujours toutes les routes gardées. C'est un argument de plus pour que le modèle vienne du fragment plutôt que d'être reconstitué par chaque satellite : je l'ai reconstitué depuis le code du rate limiter, qui écrit { key, count, lastRequest } et ne mentionne jamais id, et je me suis trompé exactement là où le socle ne se tromperait pas.

lastRequest est bien un entier de millisecondes en BigInt, ce point tenait.

Correction sur la forme : mon premier message donnait `key` en clé primaire. C'est faux, et l'erreur est instructive. La définition réelle est dans `@better-auth/core/src/db/get-tables.ts` : ```ts const rateLimitTable = { rateLimit: { modelName: options.rateLimit?.modelName || 'rateLimit', fields: { key: { type: 'string', unique: true, required: true }, count: { type: 'number', required: true }, lastRequest: { type: 'number', bigint: true, required: true, defaultValue: () => Date.now() }, }, }, } ``` `key` porte l'**unicité**, pas la clé primaire. better-auth donne un `id` à **chaque** modèle, implicitement, et vérifie sa présence au démarrage. Le fragment correct est donc : ```prisma model RateLimit { id String @id key String @unique count Int lastRequest BigInt @@map("rate_limit") } ``` Sans le `id`, le message change mais pas le symptôme : ``` Prisma schema mismatch Missing columns rateLimit.id ``` et le service refuse toujours toutes les routes gardées. C'est un argument de plus pour que le modèle vienne du fragment plutôt que d'être reconstitué par chaque satellite : je l'ai reconstitué depuis le code du rate limiter, qui écrit `{ key, count, lastRequest }` et ne mentionne jamais `id`, et je me suis trompé exactement là où le socle ne se tromperait pas. `lastRequest` est bien un entier de millisecondes en `BigInt`, ce point tenait.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Infrastructure/eqs-platform#88
No description provided.