@contract-kit/provider-auth-better-auth
v0.1.1
Published
Better Auth provider for contract-kit - adds auth port for authentication and session management
Maintainers
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 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.authwithgetSession,getUser, andrequireUsermethods - 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
bun add @contract-kit/provider-auth-better-auth better-authTypeScript 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:
// 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 Hex
Register the provider when creating your Hex app:
// app/lib/server.ts
import { createServer } from "@contract-kit/server";
import { createAuthBetterAuthProvider } from "@contract-kit/provider-auth-better-auth";
import { auth } from "./auth";
import type { AppPorts } from "./ports";
const basePorts = createPortsBuilder<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 };
},
});5. Optional: Check auth in use cases
You can also check authentication in use cases:
// app/use-cases/get-user-profile.ts
export const getUserProfile = createUseCase(
async (ctx: AppCtx, userId: string) => {
// Get the user from the request (if needed)
const currentUser = await ctx.ports.auth.getUser(ctx.req);
if (!currentUser) {
throw new Error("Unauthorized");
}
// Fetch user profile logic...
},
);API Reference
AuthPort<User>
The auth port interface exposed on ctx.ports.auth:
getSession(req: Request): Promise<AuthSession<User> | 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 middleware or use cases that require authentication.
const user = await ctx.ports.auth.requireUser(req);
// user is guaranteed to exist hereThrows: Error with message "[Auth] Unauthorized" if not authenticated.
AuthSession<User>
Represents an authenticated session:
interface AuthSession<User = unknown> {
user: User;
}createAuthBetterAuthProvider(auth)
Factory function that creates the provider:
function createAuthBetterAuthProvider<User = unknown>(
auth: BetterAuthServer<User>
): ServiceProviderParameters:
auth: A Better Auth server instance configured in your application
Returns: A Contract Kit provider that can be registered with Hex
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 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);
});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) => {
try {
return await ctx.ports.auth.requireUser(req);
} catch (error) {
throw new UnauthorizedError();
}
};Multiple authentication strategies
If you need multiple auth strategies (e.g., JWT + session), you can:
- Configure Better Auth with multiple strategies
- 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/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 = 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() };
},
});License
MIT
