Correction de l'erreur Better Auth 'Model session not found in schema'
Correction de l'erreur Better Auth 'Model session not found in schema'
Analyse approfondie et résolution de l'erreur de schéma Better Auth avec Cloudflare Workers, KV Secondary Storage, Dash Plugin et Drizzle Adapter
1. Description du Problème
Lors des appels aux endpoints d’authentification (/api/auth/sign-in/email, /api/auth/dash/*), le worker Cloudflare amn-dev ou le serveur local renvoyait l’erreur critique suivante :
ERROR [Better Auth]: BetterAuthError BetterAuthError: Model "session" not found in schema# SERVER_ERROR: BetterAuthError: Model "session" not found in schema2. Analyse des Causes Racines
L’investigation approfondie de @better-auth/core (getDefaultModelName.mjs), @better-auth/drizzle-adapter et du plugin dash() (@better-auth/infra) a révélé deux causes racines combinées :
Cause 1 : Module Namespace vs Objet de Schéma Destructuré dans drizzleAdapter1/2
Better Auth résout les noms des modèles internes (user, session, account, verification) en recherchant les clés correspondantes dans l’objet schema fourni à l’adaptateur :
const getDefaultModelName = (model) => { const resolve = (candidate) => { if (schema[candidate]) return candidate; return Object.entries(schema).find(([_, f]) => f.modelName === candidate)?.[0]; }; const m = resolve(model); if (!m) throw new BetterAuthError(`Model "${model}" not found in schema`); return m;};Cause 1 : Module Namespace vs Objet de Schéma Destructuré dans drizzleAdapter2/2
Lorsque le schéma est importé via import * as schema from "../db/schema", l’objet transmis est un ES Module Namespace contenant des métadonnées de module et des objets relations Drizzle en premier niveau. Cela perturbe la résolution de getDefaultModelName dans l’adaptateur Drizzle.
Cause 2 : Interaction entre secondaryStorage (KV) et le plugin dash()
Lorsque secondaryStorage (Cloudflare KV via createKVStorage(env.AUTH_KV)) est activé sans l’option storeSessionInDatabase: true :
- Better Auth délègue la gestion des sessions au KV et désactive l’enregistrement du modèle
sessiondans le schéma de l’adaptateur de base de données (D1). - Cependant, le plugin
dash()(Better Auth Infrastructure, activé lorsqueBETTER_AUTH_API_KEYest présent sur Cloudflare) effectue des appels internes de gestion et d’analyse sur l’adaptateur DB (ctx.context.adapter.findMany({ model: "session" })). - L’adaptateur DB ne trouvant pas le modèle
sessiondans son schéma interne, lève immédiatement l’exceptionModel "session" not found in schema.
3. Solution Appliquée (DOC → SPEC → CODE → TEST → INTÉG)
La correction a été appliquée dans apps/web/src/auth/index.ts :
A. Passage propre du schéma Drizzle
Importation déstructurée des tables et relations Drizzle :
import { user, session, account, verification, userRelations, sessionRelations, accountRelations} from "../db/schema";B. Activation de storeSessionInDatabase: true
Garantit la présence continue du modèle session dans le schéma de l’adaptateur DB même si Cloudflare KV est utilisé en stockage secondaire :
session: { modelName: "session", expiresIn: 60 * 60 * 24 * 7, // 7 jours updateAge: 60 * 60 * 24, // 1 jour storeSessionInDatabase: true // Maintenance du schéma DB pour le plugin dash()},C. Déclaration explicite des modelName
Déclaration explicite des noms de modèles dans les options globales de betterAuth :
user: { modelName: "user" },session: { modelName: "session", ... },account: { modelName: "account", accountLinking: { enabled: false } },verification: { modelName: "verification" }Code complet dans apps/web/src/auth/index.ts1/2
Code complet dans apps/web/src/auth/index.ts2/2
export function createAuth(env: any) { const db = drizzle(env.AUTH_DB, { schema: { user, session, account, verification } });
const isProd = env.ENVIRONMENT === 'prod'; const trustedOriginsEnv = env.BETTER_AUTH_TRUSTED_ORIGINS || ''; const trustedOrigins = trustedOriginsEnv ? trustedOriginsEnv.split(',').map((o: string) => o.trim()) : [];
if (!isProd) { trustedOrigins.push('http://localhost:4321', 'http://localhost:8787'); }
const plugins: any[] = [admin()];
if (env.BETTER_AUTH_API_KEY) { plugins.push(dash({ apiKey: env.BETTER_AUTH_API_KEY, activityTracking: { enabled: true, updateInterval: 300000 // 5 minutes } })); }
return betterAuth({ database: drizzleAdapter(db, { provider: "sqlite", schema: { user, session, account, verification, userRelations, sessionRelations, accountRelations, }, usePlural: false }), secondaryStorage: env.AUTH_KV ? createKVStorage(env.AUTH_KV) : undefined, baseURL: env.BETTER_AUTH_URL || (isProd ? 'https://amn-dev.anderson-metsanou.workers.dev/api/auth' : 'http://localhost:8787/api/auth'), trustedOrigins, user: { modelName: "user", }, session: { modelName: "session", expiresIn: 60 * 60 * 24 * 7, updateAge: 60 * 60 * 24, storeSessionInDatabase: true }, account: { modelName: "account", accountLinking: { enabled: false } }, verification: { modelName: "verification", }, emailAndPassword: { enabled: true, requireEmailVerification: false }, plugins });}4. Tests et Validation Empirique
A. Contrôle de Qualité Code
task typecheck # 0 erreurs, 0 avertissements sur 55 fichiersnpx vitest run # 3/3 tests unitaires passésB. Validation Locale (Wrangler Dev - http://localhost:8787)
- Connexion (
/api/auth/sign-in/email) :HTTP 200 OK, génération du cookieamn-auth.session_token. - Récupération de session (
/api/auth/get-session) :HTTP 200 OK, retour de la session et des données utilisateur.
C. Validation Distante (Cloudflare Worker amn-dev)1/2
curl -i -X POST https://amn-dev.anderson-metsanou.workers.dev/api/auth/sign-in/email \ -H "Content-Type: application/json" \ -d '{"email":"andsinusmomega@gmail.com","password":"Debian13"}'Résultat : HTTP/2 200 OK
C. Validation Distante (Cloudflare Worker amn-dev)2/2
{ "redirect": false, "token": "9Ng3ypYV0f6Ix1tWSAU7sUl4wiOxcpsq", "user": { "id": "mWic8NK0CANSa22hxCHS5ZMdGuLZlYfS", "name": "omega1", "email": "andsinusmomega@gmail.com", "emailVerified": true, "role": "user" }}5. Recommandations et Bonnes Pratiques
- Toujours définir
storeSessionInDatabase: truelorsquesecondaryStorageest utilisé conjointement avec des plugins de gestion d’infrastructure (dash()). - Passer un schéma déstructuré à
drizzleAdapterplutôt qu’un import d’espace de nommage (import * as schema). - Spécifier les
modelNameexplicites dans la configurationbetterAuthlors de l’utilisation de tables au singulier (usePlural: false).