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

@beignet/provider-auth-better-auth

v0.0.3

Published

Better Auth provider for Beignet - adds auth port for authentication and session management

Readme

@beignet/provider-auth-better-auth

Better Auth provider for Beignet applications.

The provider wraps an already-configured Better Auth server instance and exposes the shared AuthPort from @beignet/core/ports on ctx.ports.auth. Your app still owns Better Auth configuration, database schema, and auth routes.

Overview

What this provider does:

  • Wraps a Better Auth instance with a simple, stable API
  • Extends ports.auth with getSession, getUser, and requireUser methods
  • Maintains type safety for your custom User type
  • Records auth checks in devtools when devtools is installed

What this provider does NOT do:

  • Define database schema (you own your user table)
  • Define the User type (you define it in your app)
  • Configure Better Auth (secrets, session strategy, etc. stay in your app)
  • Implement login/signup routes (use Better Auth's routes directly)
  • Manage RBAC/permissions (that's application logic)

Install

bun add @beignet/core @beignet/provider-auth-better-auth better-auth

Setup

1. Configure Better Auth in your app

First, set up Better Auth with your database and configuration:

// lib/better-auth.ts
import { betterAuth } from "better-auth";
import { db } from "./db"; // Your Drizzle/Prisma/etc. client

export const auth = betterAuth({
  database: db,
  emailAndPassword: {
    enabled: true,
  },
  // ...other Better Auth configuration
});

2. Define your ports type

Own the public session shape in your app, then add the auth port to your application's ports type:

// ports/auth.ts
import type { AuthPort as BeignetAuthPort } from "@beignet/core/ports";

export type AuthUser = {
  id: string;
  name?: string | null;
  email?: string | null;
  image?: string | null;
};

export type AuthSessionMetadata = unknown;

export type AuthPort = BeignetAuthPort<AuthUser, AuthSessionMetadata>;
// ports/index.ts
import type { AuthPort } from "./auth";

export type AppPorts = {
  auth: AuthPort;
  // ...other ports (db, mailer, eventBus, etc.)
};

3. Wire the provider into Beignet

Register the provider when creating your server:

// server/providers.ts
import { createAuthBetterAuthProvider } from "@beignet/provider-auth-better-auth";
import { auth } from "@/lib/better-auth";

export const providers = [
  createAuthBetterAuthProvider(auth),
  // ...other providers
];
// server/index.ts
import { createNextServer } from "@beignet/next";
import { definePorts } from "@beignet/core/ports";
import { routes } from "@/server/routes";
import { providers } from "./providers";

const appPorts = definePorts({
  // add your app's other ports here
});

export const server = await createNextServer({
  ports: appPorts,
  providers,
  createContext: async ({ ports }) => ({ ports }),
  routes,
});

4. Use the auth port in route hooks

Use createAuthHooks(...) to create route-scoped auth hooks:

// server/auth-hooks.ts
import { createAuthHooks } from "@beignet/core/server";

export const auth = createAuthHooks<AppContext, { user: CurrentUser }>({
  resolve: async ({ ctx, req }) => {
    const session = await ctx.ports.auth.getSession(req);

    return session ? { user: session.user } : null;
  },
});

Then attach the hooks where routes are wired:

export const accountRoutes = defineRouteGroup<AppContext>()({
  name: "account",
  hooks: [auth.required()],
  routes: [
    {
      contract: getProfile,
      handle: async ({ ctx }) => getProfileUseCase.run({ ctx }),
    },
  ],
});

5. Optional: check auth in use cases

You can also check authentication in use cases:

// features/users/use-cases/get-profile.ts
import { createUseCase } from "@beignet/core/application";
import { z } from "zod";
import { requireUser } from "@/lib/auth";

const UserProfileSchema = z.object({
  id: z.string(),
  email: z.string().email(),
});

const useCase = createUseCase<AppCtx>();

export const getUserProfile = useCase
  .query("users.profile")
  .input(z.object({ userId: z.string() }))
  .output(UserProfileSchema)
  .run(async ({ ctx, input }) => {
    requireUser(ctx);

    return ctx.ports.db.users.getProfile(input.userId);
  });

In the standard app shape, createContext reads the request once with ctx.ports.auth.getSession(req) and stores the result on ctx.auth. Use cases then call an app-owned helper such as requireUser(ctx) instead of depending on the raw request.

Devtools

When @beignet/devtools is installed before this provider, auth checks appear under the dashboard's Auth watcher.

The provider records auth.getSession, auth.getUser, and auth.requireUser events with the operation, authenticated status, and duration. User and session objects are not recorded. Provider failures are recorded with .failed event names and the original error is rethrown.

API reference

AuthPort<User, Session>

The provider implements the auth port interface exported by @beignet/core/ports:

getSession(req: Request): Promise<AuthSession<User, Session> | null>

Get the current session from a Request. Returns null if not authenticated.

const session = await ctx.ports.auth.getSession(req);
if (session) {
  console.log(session.user);
}

getUser(req: Request): Promise<User | null>

Get the current user from a Request. Returns null if not authenticated.

This is a convenience method that extracts the user from the session.

const user = await ctx.ports.auth.getUser(req);
if (user) {
  console.log(user.email);
}

requireUser(req: Request): Promise<User>

Require an authenticated user. Throws an error if not authenticated.

Use this in lifecycle hooks or use cases that require authentication.

const user = await ctx.ports.auth.requireUser(req);
// user is guaranteed to exist here

Throws: AuthUnauthorizedError from @beignet/core/ports if not authenticated. When this error reaches Beignet's server runtime, it is returned as a framework-owned 401 response with the standard error envelope.

AuthSession<User, Session>

Represents an authenticated session:

interface AuthSession<User = unknown, Session = unknown> {
  user: User;
  session?: Session;
}

createAuthBetterAuthProvider(auth)

Factory function that creates the provider:

function createAuthBetterAuthProvider<User = unknown, Session = unknown>(
  auth: BetterAuthServer<User, Session>
): ServiceProvider

Parameters:

  • auth: A Better Auth server instance configured in your application

Returns: A Beignet provider that can be registered with the server

Advanced usage

Route-scoped authentication

Use createAuthHooks(...) from @beignet/core/server to enforce auth beside the contract-to-use-case wiring:

const users = createContractGroup();

const getProfile = users
  .get("/api/profile")
  .responses({
    200: z.object({ name: z.string() }),
  })
  .meta({ auth: "required" });

const auth = createAuthHooks<AppContext, { user: CurrentUser }>({
  resolve: async ({ ctx, req }) => {
    const session = await ctx.ports.auth.getSession(req);

    return session ? { user: session.user } : null;
  },
});

export const userRoutes = defineRouteGroup<AppContext>()({
  name: "users",
  hooks: [auth.required()],
  routes: [{ contract: getProfile, handle: getProfileHandler }],
});

Custom error types

You can wrap requireUser to throw custom error types:

class UnauthorizedError extends Error {
  constructor() {
    super("Unauthorized");
    this.name = "UnauthorizedError";
  }
}

export const requireAuth = async (
  req: Request,
  auth: { requireUser: (req: Request) => Promise<unknown> },
) => {
  try {
    return await auth.requireUser(req);
  } catch (error) {
    throw new UnauthorizedError();
  }
};

Multiple authentication strategies

If you need multiple auth strategies (e.g., JWT + session), you can:

  1. Configure Better Auth with multiple strategies
  2. Or create multiple providers (e.g., createAuthBetterAuthProvider(sessionAuth) + createAuthJWTProvider(jwtAuth))

Better Auth supports multiple strategies out of the box, so the first approach is recommended.

Type safety

The provider maintains full type safety for your custom User type:

type MyUser = {
  id: string;
  email: string;
  role: "admin" | "user";
};

const authProvider = createAuthBetterAuthProvider<MyUser>(auth);

// Later, in your routes:
const user = await ctx.ports.auth.requireUser(req);
// user.role is typed as "admin" | "user"

Integration with Better Auth routes

Better Auth provides its own route handlers for login, signup, etc. You can mount these alongside your Beignet routes:

// Next.js App Router example
import { auth } from "@/lib/better-auth";

// Better Auth handles /api/auth/*
export const { GET, POST } = auth.handler;

// Your Beignet routes handle /api/app/*
// (mounted separately)

See the Better Auth documentation for details on route configuration.

Examples

Basic setup

import { betterAuth } from "better-auth";
import { createNextServer } from "@beignet/next";
import { definePorts } from "@beignet/core/ports";
import { createAuthBetterAuthProvider } from "@beignet/provider-auth-better-auth";
import { routes } from "@/server/routes";

const auth = betterAuth({ database: db });
const appPorts = definePorts({});

const server = await createNextServer({
  ports: appPorts,
  providers: [createAuthBetterAuthProvider(auth)],
  createContext: async ({ ports }) => ({ ports }),
  routes,
});

Protecting routes

const authHook = {
  name: "auth",
  beforeHandle: async ({ req, ctx }) => {
    const user = await ctx.ports.auth.requireUser(req);
    return { ctx: { ...ctx, user } };
  },
};

const server = await createNextServer({
  ports: appPorts,
  providers: [createAuthBetterAuthProvider(auth)],
  hooks: [authHook],
  createContext: async ({ ports }) => ({ ports }),
  routes,
});

Optional authentication

const listData = async ({ req, ctx }) => {
  const user = await ctx.ports.auth.getUser(req);

  if (user) {
    return {
      status: 200,
      body: { data: getPersonalizedData(user) },
    };
  }

  return {
    status: 200,
    body: { data: getPublicData() },
  };
};

License

MIT