Skip to main content

Overview

The Better Auth provider wraps an already-configured Better Auth server instance and exposes a typed AuthPort 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
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

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

Setup

1. Configure Better Auth in your app

First, set up Better Auth with your database and configuration:
// app/lib/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:
// app/lib/ports.ts
import type { AuthPort } from "@contract-kit/provider-auth-better-auth";
import type { AuthUser } from "./auth";

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

3. Wire the provider into your server

Register the provider when creating your server:
// app/lib/server.ts
import { createServer, definePorts } from "@contract-kit/server";
import { createAuthBetterAuthProvider } from "@contract-kit/provider-auth-better-auth";
import { auth } from "./auth";
import type { AppPorts } from "./ports";

const basePorts = definePorts<AppPorts>({});

export const app = createServer({
  ports: basePorts,
  providers: [
    createAuthBetterAuthProvider(auth),
    // ...other providers
  ],
});

4. Use the auth port in middleware

Create middleware to protect routes:
// app/lib/middleware/auth.ts
import type { AppCtx } from "./ctx";

export const ensureAuth = app.router.middleware<AppCtx>(
  async ({ req, ctx, next }) => {
    const user = await ctx.ports.auth.requireUser(req);
    return next({ ...ctx, user });
  },
);
Then use it in your routes:
const protectedRoute = app.router.route({
  method: "GET",
  path: "/api/profile",
  middleware: [ensureAuth],
  handler: async ({ ctx }) => {
    // ctx.user is now available and typed
    return { user: ctx.user };
  },
});

API Reference

AuthPort<User>

The auth port interface exposed on ctx.ports.auth:

getSession(req: Request)

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)

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)

Require an authenticated user. Throws an error if not authenticated. Use this in middleware or use cases that require authentication.
const user = await ctx.ports.auth.requireUser(req);
// user is guaranteed to exist here
Throws: Error with message "[Auth] Unauthorized" if not authenticated.

createAuthBetterAuthProvider(auth)

Factory function that creates the provider:
function createAuthBetterAuthProvider<User = unknown>(
  auth: BetterAuthServer<User>
): ServiceProvider
Parameters:
  • auth: A Better Auth server instance configured in your application
Returns: A Contract Kit provider that can be registered with your server

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

Protecting routes

const ensureAuth = app.router.middleware(async ({ req, ctx, next }) => {
  const user = await ctx.ports.auth.requireUser(req);
  return next({ ...ctx, user });
});

const protectedRoute = app.router.route({
  method: "GET",
  path: "/api/profile",
  middleware: [ensureAuth],
  handler: async ({ ctx }) => {
    return { user: ctx.user };
  },
});

Optional authentication

const optionalAuthRoute = app.router.route({
  method: "GET",
  path: "/api/data",
  handler: async ({ req, ctx }) => {
    const user = await ctx.ports.auth.getUser(req);
    
    if (user) {
      // Return personalized data
      return { data: getPersonalizedData(user) };
    }
    
    // Return public data
    return { data: getPublicData() };
  },
});

Policy-based authorization

You can integrate the auth port with a policy system based on contract metadata:
// Define a contract with auth metadata
const contract = ContractBuilder()
  .input(z.object({ id: z.string() }))
  .output(z.object({ name: z.string() }))
  .meta({ auth: "required" })
  .build();

// Create middleware that checks the meta
const policyMiddleware = app.router.middleware(async ({ req, ctx, contract, next }) => {
  if (contract.meta?.auth === "required") {
    const user = await ctx.ports.auth.requireUser(req);
    return next({ ...ctx, user });
  }
  return next(ctx);
});

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

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"

Next Steps