Beignet API reference
    Preparing search index...

    Module @beignet/provider-auth-better-auth

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

    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)
    bun add @beignet/core @beignet/provider-auth-better-auth better-auth
    

    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
    });

    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.)
    };

    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,
    });

    Use createAuthHooks(...) to protect routes that declare .meta({ auth: "required" }):

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

    export const authHooks = createAuthHooks<AppContext>({
    assign: ({ ctx, session }) => ({
    ...ctx,
    auth: session,
    user: session?.user ?? null,
    }),
    });

    Then register it on your server:

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

    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.

    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.

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

    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);
    }

    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);
    }

    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.

    Represents an authenticated session:

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

    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

    Use createAuthHooks(...) from @beignet/core/server to enforce contract authentication metadata through the shared auth port:

    // Define a contract with auth metadata
    const users = createContractGroup();

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

    const authHooks = createAuthHooks<AppContext>({
    assign: ({ ctx, session }) => ({
    ...ctx,
    auth: session,
    user: session?.user ?? null,
    }),
    });

    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();
    }
    };

    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.

    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"

    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.

    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,
    });
    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,
    });
    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() },
    };
    };

    MIT

    Type Aliases

    BetterAuthServer

    Functions

    createAuthBetterAuthProvider