Files
better-auth/packages/effect
dallenpyrah f735441ad2 feat: add Effect/Platform native integration package
- Add @better-auth/effect package with Effect-native auth operations
- All auth operations return Effects with typed errors
- HttpApi middleware integration (AuthMiddleware, SessionMiddleware, etc.)
- Session context tags (CurrentUser, CurrentSession, AuthSession)
- Typed error classes with HTTP status codes
- Documentation for Effect/Platform integration

Closes #7338
2026-01-13 14:46:14 -07:00
..
…
…
…

@better-auth/effect

Effect-native integration for Better Auth with Effect/Platform.

Features

  • 🎯 Effect-Native - All auth operations return Effects with typed errors
  • 🔒 Type-Safe - Full type inference from your auth configuration
  • 🧩 HttpApi Integration - Seamless middleware for Effect/Platform's HttpApi
  • 💉 Dependency Injection - Uses Effect's Context/Layer system
  • ⚡ Composable - Works naturally with Effect's ecosystem

Installation

npm install @better-auth/effect better-auth effect @effect/platform
# or
pnpm add @better-auth/effect better-auth effect @effect/platform
# or
bun add @better-auth/effect better-auth effect @effect/platform

Quick Start

1. Configure Better Auth

// auth.ts
import { betterAuth } from "better-auth"
import { drizzleAdapter } from "better-auth/adapters/drizzle"
import { db } from "./db"

export const auth = betterAuth({
  database: drizzleAdapter(db, { provider: "pg" }),
  emailAndPassword: { enabled: true },
})

export type Auth = typeof auth

2. Create the Effect Layer

// auth-effect.ts
import { layerFromAuth } from "@better-auth/effect"
import { auth } from "./auth"

export const BetterAuthLive = layerFromAuth(auth)

// Re-export for convenience
export { BetterAuth, CurrentUser, CurrentSession } from "@better-auth/effect"

3. Use in HttpApi

// api.ts
import {
  HttpApi,
  HttpApiBuilder,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpServer,
  HttpMiddleware,
} from "@effect/platform"
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Effect, Layer, Schema } from "effect"
import { createServer } from "node:http"
import {
  AuthMiddleware,
  AuthMiddlewareLive,
  CurrentUser,
  betterAuthApp,
} from "@better-auth/effect"
import { BetterAuthLive } from "./auth-effect"

// Define a user profile schema
class UserProfile extends Schema.Class<UserProfile>("UserProfile")({
  id: Schema.String,
  email: Schema.String,
  name: Schema.String,
}) {}

// Protected API group
class UsersApi extends HttpApiGroup.make("users")
  .add(
    HttpApiEndpoint.get("me", "/me")
      .addSuccess(UserProfile)
  )
  .middleware(AuthMiddleware) // Requires authentication
  .prefix("/users")
{}

class MyApi extends HttpApi.make("my-api").add(UsersApi) {}

// Implement handlers
const UsersLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
  handlers.handle("me", () =>
    Effect.gen(function* () {
      const user = yield* CurrentUser
      return new UserProfile({
        id: user.id,
        email: user.email,
        name: user.name,
      })
    })
  )
).pipe(Layer.provide(AuthMiddlewareLive))

// Build and serve
const ApiLive = HttpApiBuilder.api(MyApi).pipe(
  Layer.provide(UsersLive)
)

// Mount better-auth handler
const RouterLive = HttpApiBuilder.Router.use((router) =>
  Effect.gen(function* () {
    const authApp = yield* betterAuthApp
    yield* router.mountApp("/api/auth", authApp)
  })
)

HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
  Layer.provide(RouterLive),
  Layer.provide(ApiLive),
  Layer.provide(BetterAuthLive),
  HttpServer.withLogAddress,
  Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
  Layer.launch,
  NodeRuntime.runMain
)

API Reference

Service

BetterAuth

The main service tag for accessing auth operations.

import { BetterAuth } from "@better-auth/effect"

const program = Effect.gen(function* () {
  const auth = yield* BetterAuth
  
  // Get session
  const session = yield* auth.getSession(headers)
  
  // Sign in
  const result = yield* auth.signInEmail({
    email: "user@example.com",
    password: "password",
  })
  
  // Sign up
  const newUser = yield* auth.signUpEmail({
    email: "new@example.com",
    password: "password",
    name: "New User",
  })
})

layer(options)

Creates a layer from Better Auth options.

import { layer } from "@better-auth/effect"

const BetterAuthLive = layer({
  database: drizzleAdapter(db, { provider: "pg" }),
  emailAndPassword: { enabled: true },
})

layerFromAuth(auth)

Creates a layer from an existing Better Auth instance.

import { layerFromAuth } from "@better-auth/effect"
import { auth } from "./auth"

const BetterAuthLive = layerFromAuth(auth)

Session Context

CurrentUser

Context tag for the authenticated user.

import { CurrentUser } from "@better-auth/effect"

const handler = Effect.gen(function* () {
  const user = yield* CurrentUser
  return { id: user.id, email: user.email }
})

CurrentSession

Context tag for the session data.

import { CurrentSession } from "@better-auth/effect"

const handler = Effect.gen(function* () {
  const session = yield* CurrentSession
  return { token: session.token, expiresAt: session.expiresAt }
})

AuthSession

Context tag for both user and session.

import { AuthSession } from "@better-auth/effect"

const handler = Effect.gen(function* () {
  const { user, session } = yield* AuthSession
})

OptionalUser

Context tag for optional authentication.

import { OptionalUser } from "@better-auth/effect"

const handler = Effect.gen(function* () {
  const maybeUser = yield* OptionalUser
  if (maybeUser) {
    return { message: `Hello, ${maybeUser.name}!` }
  }
  return { message: "Hello, guest!" }
})

Middleware

AuthMiddleware / AuthMiddlewareLive

Requires authentication, provides CurrentUser.

class ProtectedApi extends HttpApiGroup.make("protected")
  .add(HttpApiEndpoint.get("data", "/data").addSuccess(Data))
  .middleware(AuthMiddleware)
{}

// Provide the implementation
Layer.provide(AuthMiddlewareLive)

SessionMiddleware / SessionMiddlewareLive

Requires authentication, provides CurrentSession.

FullAuthMiddleware / FullAuthMiddlewareLive

Requires authentication, provides AuthSession (user + session).

OptionalAuthMiddleware / OptionalAuthMiddlewareLive

Optional authentication, provides OptionalUser.

AllAuthMiddlewareLive

Combined layer for all middleware types.

Layer.provide(AllAuthMiddlewareLive)

Handler Mounting

betterAuthApp

Creates an HttpApp from the BetterAuth service.

import { betterAuthApp } from "@better-auth/effect"

const program = Effect.gen(function* () {
  const authApp = yield* betterAuthApp
  // Mount at /api/auth
})

betterAuthAppFrom(auth)

Creates an HttpApp directly from a Better Auth instance.

import { betterAuthAppFrom } from "@better-auth/effect"
import { auth } from "./auth"

const authApp = betterAuthAppFrom(auth)

Errors

All errors are Effect Schema TaggedError classes with HTTP status codes:

import {
  Unauthorized,       // 401
  InvalidCredentials, // 401
  SessionExpired,     // 401
  Forbidden,          // 403
  UserNotFound,       // 404
  EmailAlreadyExists, // 409
  RateLimited,        // 429
} from "@better-auth/effect"

// Handle specific errors
program.pipe(
  Effect.catchTag("Unauthorized", (e) =>
    Effect.succeed({ error: "Please log in" })
  ),
  Effect.catchTag("RateLimited", (e) =>
    Effect.succeed({ error: `Try again in ${e.retryAfter}s` })
  )
)

Advanced Usage

Typed Session Context

For exact type inference from your auth config:

import { makeSessionContext } from "@better-auth/effect"
import type { auth } from "./auth"

const { CurrentUser, CurrentSession, AuthSession } = makeSessionContext<typeof auth>()

// CurrentUser.Type is now exactly your user type

If you've customized the session cookie name:

// Create custom middleware with your cookie name
import { HttpApiMiddleware, HttpApiSecurity } from "@effect/platform"
import { Unauthorized } from "@better-auth/effect"

class CustomAuthMiddleware extends HttpApiMiddleware.Tag<CustomAuthMiddleware>()(
  "CustomAuthMiddleware",
  {
    failure: Unauthorized,
    provides: CurrentUser,
    security: {
      cookie: HttpApiSecurity.apiKey({
        in: "cookie",
        key: "my-custom-session-cookie",
      }),
    },
  }
) {}

License

MIT