@kenzuya/nestelia-better-auth
v2.8.0
Published
Better Auth integration for Nestelia and Elysia
Maintainers
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-wsimport { 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-authand framework imports to@kenzuya/nesteliaor its subpaths. - Bootstrap with
createElysiaApplicationand explicitly apply@UseGuards(AuthGuard). - Use
AuthModule.forRoot({ auth }). The positional overload anddisableGlobalAuthGuardoption are removed.PublicandOptionalremain aliases for the corresponding auth decorators. - Remove
bodyParser,disableBodyParser, andenableRawBodyParser. 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
GraphQLModulefrom@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 testBuild 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.
