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

@contract-kit/provider-auth-better-auth

v1.0.0

Published

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

Readme

@contract-kit/provider-auth-better-auth

Better Auth provider for Contract Kit - adds authentication and session management via Better Auth.

Overview

This provider wraps an already-configured Better Auth server instance and exposes the shared AuthPort from @contract-kit/ports on ctx.ports.auth. It follows the ports & adapters pattern, providing a stable interface for authentication while letting your application own the configuration.

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)

Installation

bun add @contract-kit/ports @contract-kit/provider-auth-better-auth better-auth

TypeScript requirements

This package requires TypeScript 5.0 or higher for proper type inference.

Usage

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

// Export your user type
export type AuthUser = typeof auth.$Infer.Session.user;

2. Define your ports type

Add the auth port to your application's ports type:

// ports/index.ts
import type { AuthPort } from "@contract-kit/ports";
import type { AuthUser } from "@/lib/better-auth";

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

3. Wire the provider into Contract Kit

Register the provider when creating your server:

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

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

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

export const server = await createNextServer({
  ports: basePorts,
  providers,
  createContext: async ({ ports }) => ({ ports }),
  mapUnhandledError: () => ({
    status: 500,
    body: {
      code: "INTERNAL_SERVER_ERROR",
      message: "Internal server error",
    },
  }),
});

4. Use the auth port in beforeHandle hooks

Create a hook to protect routes:

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

Then register it on your server:

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

5. Optional: check auth in use cases

You can also check authentication in use cases:

// use-cases/users/get-profile.ts
import { createUseCase } from "@contract-kit/application";
import { z } from "zod";

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 }) => {
    const currentUser = await ctx.ports.auth.requireUser(ctx.req);

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

Devtools

When @contract-kit/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 @contract-kit/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 @contract-kit/ports if not authenticated. When this error reaches Contract Kit'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 Contract Kit provider that can be registered with the server

Advanced usage

Policy-based authorization

You can integrate the auth port with a policy system based on contract metadata:

// 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 ensureAuth = async ({ req, ctx, contract }) => {
  if (contract.metadata?.auth !== "required") {
    return;
  }

  const user = await ctx.ports.auth.requireUser(req);
  return { ctx: { ...ctx, user } };
};

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 Contract Kit routes:

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

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

// Your Contract Kit 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 { createServer } from "@contract-kit/server";
import { createAuthBetterAuthProvider } from "@contract-kit/provider-auth-better-auth";

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

const app = await createServer({
  ports: basePorts,
  providers: [createAuthBetterAuthProvider(auth)],
  createContext: async ({ ports }) => ({ ports }),
  mapUnhandledError: () => ({
    status: 500,
    body: {
      code: "INTERNAL_SERVER_ERROR",
      message: "Internal server error",
    },
  }),
});

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 createServer({
  ports: basePorts,
  providers: [createAuthBetterAuthProvider(auth)],
  hooks: [authHook],
  createContext: async ({ ports }) => ({ ports }),
});

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