npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kenzuya/nestelia-better-auth

v2.8.0

Published

Better Auth integration for Nestelia and Elysia

Readme

Nestelia Better Auth

Better Auth authentication and authorization for Nestelia and Elysia on Bun. Supports HTTP controllers, GraphQL resolvers, and native WebSocket gateways.

This package is a Nestelia port of Thalles Passos's nestjs-better-auth, distributed under the original MIT license.

Setup

Install the package and its runtime peers from npm:

bun add @kenzuya/nestelia-better-auth '@kenzuya/nestelia@^1.11.3' 'elysia@^1.4.30' 'better-auth@>=1.5.0 <2.0.0' 'reflect-metadata@^0.2.2'

The runtime peer dependencies are @kenzuya/nestelia ^1.11.3, Elysia ^1.4.30, Better Auth >=1.5.0 <2, and reflect-metadata ^0.2.2. Use Bun >=1.3.14. In your application's tsconfig, enable experimentalDecorators and emitDecoratorMetadata.

Basic usage

import 'reflect-metadata';
import { Controller, Get, Module, UseGuards, createElysiaApplication } from '@kenzuya/nestelia';
import { betterAuth } from 'better-auth';
import { AuthModule, AuthGuard, AllowAnonymous, Session, type UserSession } from '@kenzuya/nestelia-better-auth';

const auth = betterAuth({
  baseURL: 'http://localhost:3000',
  secret: process.env.BETTER_AUTH_SECRET,
  emailAndPassword: { enabled: true },
  // Add your Better Auth database adapter here.
});

@UseGuards(AuthGuard)
@Controller('/users')
class UsersController {
  @Get('/me')
  me(@Session() session: UserSession<typeof auth>) {
    return session.user;
  }

  @AllowAnonymous()
  @Get('/status')
  status() { return { ok: true }; }
}

@Module({ imports: [AuthModule.forRoot({ auth })], controllers: [UsersController] })
class AppModule {}

const app = await createElysiaApplication(AppModule);
await app.listen(3000);

Register one AuthModule per application. Providers are global by default; this makes them injectable throughout the application. Route protection is explicit: add @UseGuards(AuthGuard) to each protected controller, resolver, or gateway. Classes without that guard remain public.

Authorization and sessions

Method decorators override class metadata. All authorization decorators require AuthGuard.

| Decorator | Behavior | | --- | --- | | @AllowAnonymous() | Allows access regardless of session or permission requirements. | | @OptionalAuth() | Allows requests without a session; checks permissions when a session exists. | | @Roles(['admin']) | Checks user roles from the Better Auth admin plugin. | | @RequireActiveOrg() | Requires an active organization in the session. | | @OrgRoles(['owner', 'admin']) | Requires an active organization and any listed member role. | | @UserHasPermission({ permissions: { project: ['create'] } }) | Calls the admin plugin's permission API. Accepts permission as an alias for permissions, plus optional userId and role. | | @MemberHasPermission({ permissions: { project: ['create'] } }) | Checks permissions of the current member in the active organization. |

A missing session produces HTTP 401; insufficient permissions produce 403. Missing authorization plugins fail closed. Cookie authentication works directly; bearer authentication requires Better Auth's bearer plugin.

Use UserSession<typeof auth> to infer custom user and session fields. Optional sessions have type UserSession<typeof auth> | null. The guard also attaches session and user to the Web Request, available through @Req() request: AuthRequest<typeof auth>.

Inject AuthService<typeof auth> with @Inject(AuthService) to access service.api and service.instance. Both preserve the auth instance's inferred types. Import this package's Session decorator for Better Auth sessions.

Module options

| Option | Default | Purpose | | --- | --- | --- | | auth | Required | Better Auth instance. | | isGlobal | true | Makes auth providers available to other modules. | | disableControllers | false | Disables mounting auth endpoints while retaining injectable services and guards. | | disableTrustedOriginsCors | false | Leaves CORS to the application when enabled. | | middleware | None | Wraps requests to auth endpoints. |

Auth endpoints use auth.options.basePath, defaulting to /api/auth. The handler receives the original Web Request before body parsing. Other routes keep Elysia's normal parsing behavior. Status codes, redirects, and multiple Set-Cookie headers are preserved.

Automatic CORS applies only to auth endpoints. It accepts array-based trusted origins, including wildcard patterns, and handles credentialed preflight requests. Function-based trusted origins require disableTrustedOriginsCors: true and application-managed CORS.

AuthModule.forRoot({
  auth,
  middleware: async (context, next) => {
    if (context.request.headers.get('x-maintenance') === 'true') {
      return new Response('Authentication unavailable', { status: 503 });
    }
    const response = await next();
    response.headers.set('x-auth-service', 'identity');
    return response;
  },
});

Async configuration supports imports, inject, useFactory, useClass, and useExisting. A factory class implements create(): AuthModuleOptions | Promise<AuthModuleOptions>. Pass isGlobal at the registration level; return runtime options from the factory.

AuthModule.forRootAsync({
  imports: [ConfigurationModule],
  inject: [ConfigurationService],
  useFactory: async (config: ConfigurationService) => ({
    auth: config.auth,
    disableControllers: false,
  }),
});

GraphQL

Install the peers required by @kenzuya/nestelia/apollo:

bun add @apollo/server @graphql-tools/schema graphql graphql-ws
import { Injectable, Module, UseGuards } from '@kenzuya/nestelia';
import { GraphQLModule, Resolver, Query } from '@kenzuya/nestelia/apollo';
import { AuthGuard, AuthModule, Session, type UserSession } from '@kenzuya/nestelia-better-auth';

@Injectable()
@UseGuards(AuthGuard)
@Resolver()
class UserResolver {
  @Query(() => String)
  me(@Session() session: UserSession<typeof auth>) { return session.user.id; }
}

@Module({
  imports: [AuthModule.forRoot({ auth }), GraphQLModule.forRoot({ autoSchemaFile: true })],
  providers: [UserResolver],
})
class GraphqlAppModule {}

The default GraphQL context works directly. Custom contexts must expose a Web Request as req or request. The guard adds session and user to that context. Authentication errors use GraphQL extension codes UNAUTHENTICATED and FORBIDDEN. Query and mutation resolvers are covered; GraphQL subscriptions are outside this integration's tested scope.

WebSocket

import { Injectable, Module, OnMessage, UseGuards, WebSocketGateway } from '@kenzuya/nestelia';
import { AuthGuard, AuthModule, type AuthWsContext } from '@kenzuya/nestelia-better-auth';

@Injectable()
@UseGuards(AuthGuard)
@WebSocketGateway('/ws/private')
class PrivateGateway {
  @OnMessage()
  message(ws: AuthWsContext<typeof auth>, message: unknown) {
    ws.send({ userId: ws.data.user?.id, message });
  }
}

@Module({ imports: [AuthModule.forRoot({ auth })], gateways: [PrivateGateway] })
class WebSocketAppModule {}

Nestelia runs gateway guards during the HTTP upgrade. Missing or invalid credentials reject the connection with 401; insufficient permissions reject it with 403. Browser clients send cookies; clients that support custom handshake headers may send a bearer token.

Read the connection's session from ws.data.session and user from ws.data.user. Put @AllowAnonymous(), @OptionalAuth(), and permission decorators on gateway classes. Nestelia does not apply parameter decorators or per-message guards to gateway handlers. Session revocation after upgrade is not rechecked. Use separate gateways for public, optional, and protected connections.

Hooks

Register hook classes as providers, with @Injectable() and @Hook() or @DatabaseHook(). Include hooks: {} or databaseHooks: {} in Better Auth options before bootstrap. Missing configuration causes startup to fail.

import { Injectable } from '@kenzuya/nestelia';
import { Hook, BeforeHook, type AuthHookContext } from '@kenzuya/nestelia-better-auth';

@Hook()
@Injectable()
class SignupHook {
  @BeforeHook('/sign-up/email')
  before(ctx: AuthHookContext) {
    // Apply application-specific validation here.
  }
}

@BeforeHook(path?) and @AfterHook(path?) match an exact auth path; omitting the path matches every auth operation. Existing hooks run before provider hooks. Provider methods retain their injected dependencies, and inherited methods are supported.

Database decorators are BeforeCreate, AfterCreate, BeforeUpdate, AfterUpdate, BeforeDelete, and AfterDelete. Each accepts 'user', 'session', 'account', or 'verification'. Before-hooks may return { data } to transform input or false to cancel. Transformations flow to subsequent hooks; cancellation stops the chain.

Migration from the NestJS package

  • Change imports to @kenzuya/nestelia-better-auth and framework imports to @kenzuya/nestelia or its subpaths.
  • Bootstrap with createElysiaApplication and explicitly apply @UseGuards(AuthGuard).
  • Use AuthModule.forRoot({ auth }). The positional overload and disableGlobalAuthGuard option are removed. Public and Optional remain aliases for the corresponding auth decorators.
  • Remove bodyParser, disableBodyParser, and enableRawBodyParser. Configure ordinary route parsing in Elysia. Middleware now uses Web Request/Response through (context, next).
  • Replace Socket.IO gateways and clients with native WebSocket. Read sessions through ws.data; authentication is checked per connection.
  • Use GraphQLModule from @kenzuya/nestelia/apollo. HTTP, GraphQL, and WebSocket no longer require NestJS, Express, Fastify, or Socket.IO.

Verification

From this package directory:

bun install --frozen-lockfile
bun run build
bun run typecheck
bun run lint
bun run check
bun run format:check
bun run test

Build before running tests: the package smoke test imports the generated ESM output. Tests cover actual Nestelia applications, Better Auth's in-memory database, GraphQL queries/mutations, and native WebSocket handshakes. Run tests sequentially because Nestelia uses a process-wide container.

Releases

npm releases are started manually with New Build in the Buildkite UI. See the Buildkite release setup for agent requirements, npm credentials, and release instructions.