Library / SDK
ThallesP/nestjs-better-auth avatar
ThallesP/nestjs-better-auth

nestjs-better-auth: wiring Better Auth into a NestJS application

Easily add authentication to your project

638 stars89 forksTypeScriptMIT

At a glance

What is it?
The @thallesp/nestjs-better-auth package gives NestJS applications a global authentication guard, session and role decorators, and a body parser handover so Better Auth can read the raw request body, at the cost of a hard version floor on better-auth and Node.
Who is it for?
nestjs-better-auth fits a NestJS team that has already chosen Better Auth and wants guard-based route protection across REST, GraphQL and WebSockets, with role checks that do not blur system and organisation levels. It does not fit a project on Node older than 22.22.1, a team on a better-auth release older than 1.5.0, or an application that needs the built-in body parser left in place.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 20 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A thin adapter, not a replacement for Better Auth

The package is described as a comprehensive NestJS integration library for Better Auth, and the shape of the work is narrower than that phrase suggests. Better Auth does the authentication and the session handling. This library does the NestJS-specific half: it registers a guard, it exposes decorators, it mounts the Better Auth routes on the application, and it manages the body parser so that Better Auth can see the request body as raw bytes. The design assumption is that the authentication logic, the plugins and the data model are Better Auth's business, and that a NestJS developer wants them reachable through the dependency injection and decorator conventions the framework already uses. The package is MIT licensed and published as @thallesp/nestjs-better-auth, with version 2.8.0 released on 2026-09-03 and the last push to the repository on 2026-10-05.

One package manager is enough, and better-auth has a version floor

Installation is a single dependency, and the documentation lists the four package managers it supports:

bash
npm install @thallesp/nestjs-better-auth

The same package installs with yarn, pnpm or bun. What matters more is the prerequisite. The library requires better-auth at version 1.5.0 or newer, and older versions are explicitly described as deprecated and unsupported. Before you start you also need a working NestJS application and a Better Auth instance that is already installed and configured, following the Better Auth installation guide. The package metadata adds two more floors: the engines field requires Node 22.22.1 or newer, and the declared package manager is bun 1.3.14 even though the published artefact is a plain npm package. Both constraints are the kind of thing that decides a dependency before anyone argues about its API.

The body parser has to be handed over

The first setup step is the one people skip, and skipping it breaks request handling in a way that is hard to trace. NestJS's built-in body parser has to be disabled so that Better Auth can handle the raw request body, and the library re-adds the default parsers for non-auth routes so the rest of your application is unaffected:

ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    // The library will re-add the default body parsers for non-auth routes.
    bodyParser: false,
  });
  await app.listen(process.env.PORT ?? 3333);
}
bootstrap();

There is a documented side effect here. Since the built-in body parser is disabled, the rawBody option in NestFactory.create() has no effect. If you need access to req.rawBody, for example to verify a webhook signature, the setting moves into the module options instead, and the documentation points at bodyParser.rawBody there.

AuthModule.forRoot takes the parser options with it

The module is registered in the root module with the auth instance and the body parser configuration. The part worth reading is the bodyParser object, since it is where the rawBody behaviour now lives:

ts
    AuthModule.forRoot({
      auth,
      bodyParser: {
        json: { limit: "2mb" },
        urlencoded: { limit: "2mb", extended: true },
        rawBody: true,
      },
    }),

Both bodyParser.json and bodyParser.urlencoded accept parser options plus an enabled flag, so either parser can be switched off individually, and bodyParser.rawBody set to true restores the Nest-style req.rawBody support. Two adapter-specific details follow. On Fastify, bodyParser.urlencoded with extended true uses the optional peer dependency qs, which you have to install in your application if you want nested URL-encoded parsing there. And if you configure trustedOrigins, this module applies the Better Auth CORS headers for the auth routes, which matters on Fastify because the Better Auth routes are mounted through middleware and app-level @fastify/cors does not fully cover them.

Every route is protected unless you say otherwise

The default is the opposite of what most frameworks do. An AuthGuard is registered globally by this module, so all routes are protected unless you explicitly allow access with @AllowAnonymous() or mark them as optional with @OptionalAuth(). The decorators work at the method level and at the class level, so a whole controller can be opened with a single annotation. A session is read from the handler through the Session decorator:

ts
@Controller("users")
export class UserController {
  @Get("me")
  async getProfile(@Session() session: UserSession) {
    return session;
  }
}

For an optional route the session argument may be empty, so the handler checks it before using it. The security consequence of the default is worth stating plainly. A new controller in a fresh module is closed until someone opens it, which is the opposite failure mode from the one most teams are used to, and it will surface as a wall of 401 responses on the day you add a public endpoint.

GraphQL and WebSockets need the guard attached by hand

The library covers more than REST. GraphQL is supported and works the same way: the global guard applies to resolvers, and @AllowAnonymous() and @OptionalAuth() can be used on queries and mutations. WebSockets are supported too, with one difference that trips people up. Because a socket connection is not an HTTP request, the guard is not applied automatically, and you must set it for all connections, either at the Gateway level or at the Message level, using the UseGuards decorator with the AuthGuard from this package and a WebSocketGateway declaration. The README points at a test gateway under tests/shared for a full example. In other words, a WebSocket endpoint in a NestJS application is unprotected by default in a way that a REST endpoint is not, and the fix is a decorator you have to remember.

Three role decorators, kept apart on purpose

Authorisation is where the design shows its care. The library ships three role decorators and they deliberately check different things. @Roles() checks user.role only and is the system-level check tied to the Better Auth admin plugin. @RequireActiveOrg() requires an active organization, which is what you need for routes that only use activeOrganizationId for scoping. @OrgRoles() checks the active organization together with the organization member role, tied to the organization plugin. The documentation explains why they are separate: @Roles() does not check organisation member roles, and that is intentional so that an organisation admin cannot bypass system-level admin protection. A team that merged these into one decorator would create a privilege escalation path, which is exactly the reason the README flags the distinction.

Where this library is the wrong choice

Two boundaries are worth naming. The first is version pressure. The Node floor of 22.22.1 and the better-auth floor of 1.5.0 mean the library cannot be dropped into an application that is behind on either, and the better-auth side of that pair is the one that moves, since deprecated versions stop being supported rather than being patched. The second is the Fastify path, where the optional qs peer dependency, the middleware-mounted Better Auth routes and the CORS behaviour all have to be handled explicitly; the Express path in the same application is simpler. There is also a testing angle worth noting. The package scripts run the test suite twice, once with the Express adapter and once with the Fastify adapter, using a TEST_HTTP_ADAPTER environment variable to switch, and the repository keeps an example application under examples/ alongside the source and tests. If your team does not run both adapters, you are testing a narrower world than the library supports.

Editorial conclusion

nestjs-better-auth fits a NestJS team that has already chosen Better Auth and wants guard-based route protection across REST, GraphQL and WebSockets, with role checks that do not blur system and organisation levels. It does not fit a project on Node older than 22.22.1, a team on a better-auth release older than 1.5.0, or an application that needs the built-in body parser left in place. Before adopting it, disable the body parser in NestFactory.create() and move any rawBody requirement into AuthModule.forRoot(), because the rawBody option at bootstrap has no effect once the built-in parser is off, and check that your Fastify setup installs the optional qs peer dependency if you rely on extended URL-encoded parsing.

Frequently asked questions

How do I install @thallesp/nestjs-better-auth?

Add it as a normal dependency with npm install, yarn add, pnpm add or bun add. The package requires better-auth at version 1.5.0 or newer, and older versions are deprecated and unsupported.

Why do I have to disable the NestJS body parser?

Better Auth needs the raw request body, so the built-in body parser is turned off with bodyParser: false in NestFactory.create(). The library re-adds the default body parsers for non-auth routes so the rest of the application is unaffected.

How do I get req.rawBody with this library?

The rawBody option in NestFactory.create() has no effect once the built-in parser is disabled. Set bodyParser.rawBody to true in the AuthModule.forRoot() options instead, and bodyParser.json and bodyParser.urlencoded also accept an enabled flag to turn either parser off.

Are all NestJS routes protected by default?

Yes. The module registers an AuthGuard globally, so routes are protected until you add @AllowAnonymous() or @OptionalAuth(), at method or class level. The same decorators work on GraphQL resolvers and on WebSocket connections.

How do the role decorators differ in nestjs-better-auth?

@Roles() checks only user.role for system-level roles, @RequireActiveOrg() requires an active organization, and @OrgRoles() checks the organization member role as well. They are separate on purpose so an organization admin cannot bypass system-level admin protection.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. ThallesP/nestjs-better-auth on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/thallesp-nestjs-better-auth.svg)](https://hysenlabs.com/projects/thallesp-nestjs-better-auth)

Was this useful?