Skip to content
better-auth-mongoose

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-auth
CI statusbetter-auth-mongoose version on npmbetter-auth-mongoose-tenant version on npmMIT License

The 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.

  1. 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.

    better-auth #1492

  2. 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.

  3. 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.

    better-auth #6289discussion #9364

  4. 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.

    discussion #1921

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

auth.ts
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

auth.ts
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.

test/populate.test.ts
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

better-auth-mongoose version on npm

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-auth

better-auth-mongoose-tenant

better-auth-mongoose-tenant version on npm

Optional. Automatic tenant isolation.

Tenant-scoped query middleware on top of Better Auth's organization plugin. Every read and write on the models you name gets scoped to the active organization, with no .where() to forget.

pnpm add better-auth-mongoose-tenant

Recipes

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.

Compatibility matrix for better-auth-mongoose and better-auth-mongoose-tenant
DependencyVersionsVerified by
Node.js20, 22CI test matrix, every push
Better Auth1.4, 1.5, 1.6CI test matrix, every push
Mongoose6, 7, 8, 96/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.