Unofficial · community-maintained · not affiliated with Better Auth
Better Auth’s tables, as real Mongoose models.
Better Auth’s official MongoDB adapter talks to the raw mongodb driver, not Mongoose. This one doesn’t. Your user, session, and account collections become real, registered Mongoose models. .populate(), schema validation, and hooks work the way they already do everywhere else in your app.
pnpm add better-auth-mongoose mongoose better-authThe problem
Most Node backends already running MongoDB use Mongoose, not the raw driver. It’s close to universal in NestJS and Express apps. Better Auth talking straight to mongodb instead isn’t just a style mismatch. It produces four specific problems, and they’ve been open on Better Auth’s own GitHub since February 2025.
- 01
An extra dependency you didn't ask for.
Even a Mongoose-only project ends up with the raw mongodb package installed, just for Better Auth's own tables.
- 02
Two connections, no shared schema.
Better Auth writes user, session, and account documents straight through MongoClient. Your own Mongoose models never see those writes: no validation, no hooks, no virtuals.
- 03
populate() breaks.
Reference a Better-Auth-created document from your own schema, and .populate() silently fails, alongside _id type mismatches between Better Auth's ID handling and Mongoose's ObjectId.
- 04
The only fix on offer is a workaround.
Every answer in Better Auth's own discussions comes down to reaching into mongoose.connection.getClient() and handing the raw client over. That sidesteps the schema and populate problems instead of solving them.
The documented workaround, versus this
Both connect to the same database. Only one of them gives your own code a real, extensible model to work with afterward.
The workaround everyone links to
import { betterAuth } from "better-auth";
import { mongodbAdapter } from "better-auth/adapters/mongodb";
import mongoose from "mongoose";
await mongoose.connect(process.env.MONGO_URI!);
export const auth = betterAuth({
// Reach into Mongoose's own connection to hand Better Auth a raw
// client. No schema, no validation, no hooks — and now two ways
// to read the same collections.
database: mongodbAdapter(mongoose.connection.getClient().db()),
});better-auth-mongoose
import { betterAuth } from "better-auth";
import { mongooseAdapter } from "better-auth-mongoose";
import mongoose from "mongoose";
await mongoose.connect(process.env.MONGO_URI!);
export const auth = betterAuth({
database: mongooseAdapter(mongoose.connection),
});Proof, not claims
This isn’t marketing copy. It’s a real, CI-run test in the repo.
const auth = betterAuth({ database: mongooseAdapter(connection) });
const { user } = await auth.api.signUpEmail({
body: {
email: "author@example.com",
password: "correct-horse-battery-staple",
name: "Post Author",
},
});
// A consumer-defined model, with a real ObjectId ref to Better
// Auth's own "user" collection.
const Post = connection.model(
"Post",
new Schema({
title: String,
author: { type: Schema.Types.ObjectId, ref: "user", required: true },
}),
);
await Post.create({ title: "Hello, populate()", author: coerceToObjectId(user.id) });
const post = await Post.findOne({ title: "Hello, populate()" })
.populate("author")
.lean()
.exec();
post.author.email; // "author@example.com" — resolved by plain Mongoose .populate()Two packages
The adapter stands on its own. The tenant plugin is optional, and only useful if you’re already using Better Auth’s organization plugin.
better-auth-mongoose
The adapter. Start here.
Real, extensible Mongoose models for Better Auth's own tables. .populate(), schema validation, and hooks all work normally from your own application code.
pnpm add better-auth-mongoose mongoose better-authRecipes
Every snippet here is lifted straight from the packages’ own READMEs and tests, not written for this page.
Point the adapter at a connection you already opened. Nothing else changes.
import { betterAuth } from "better-auth";
import { mongooseAdapter } from "better-auth-mongoose";
import mongoose from "mongoose";
await mongoose.connect(process.env.MONGO_URI!);
export const auth = betterAuth({
database: mongooseAdapter(mongoose.connection),
emailAndPassword: { enabled: true },
});Compatibility
Peer ranges, and where CI actually exercises them. Nothing here is a claim without a workflow run behind it.
| Dependency | Versions | Verified by |
|---|---|---|
| Node.js | 20, 22 | CI test matrix, every push |
| Better Auth | 1.4, 1.5, 1.6 | CI test matrix, every push |
| Mongoose | 6, 7, 8, 9 | 6/7/8 via the tenant package's dedicated compat job; 9 via the default install used elsewhere |
Frequently asked
Is this an official Better Auth package?
No. This is a community project, not affiliated with or endorsed by the Better Auth team. It exists because the gap between Better Auth and Mongoose has been open on Better Auth's own GitHub since February 2025 (issue #1492), with no first-party fix.
Do I still need the mongodb driver installed?
No. better-auth-mongoose has zero direct dependencies of its own and never pulls in the raw mongodb driver. mongoose and better-auth are peer dependencies you already have.
Which Mongoose and Better Auth versions are supported?
Mongoose 6 through 9, and Better Auth 1.4 through 1.6, per the packages' peer ranges. CI runs the full test suite on Node 20 and 22 against all three Better Auth minors, and separately runs the tenant package's version-sensitive internals against real Mongoose 6, 7, and 8 installs; Mongoose 9 is covered by the default dev install used everywhere else.
How does better-auth-mongoose-tenant relate to the organization plugin?
It's built with the organization plugin, not instead of it. Organization gives you organizations, members, and an active organization on the session. It doesn't automatically scope your own app models (Project, Invoice, whatever you have) to that active organization — tenantScoped() is what makes that automatic instead of a .where() every service method has to remember.
What's the current stability?
Both packages are at 0.1.x. The adapter passes the official @better-auth/test-utils adapter contract suite and the CI-run populate() test in the repo; the tenant plugin has its own CI suite covering the enforcement layers described in its README. Still young enough that you should read the changelog before bumping minor versions.
What does it cost, and what's the license?
Free and open source, MIT licensed. Both packages, no paid tier.