---
title: "Custom Auth API"
url: "https://effect-auth.itsbroly.com/guides/custom-auth-api/"
description: "Bind effect-auth operations to an application-owned HTTP contract."
---



A custom auth API lets your application choose the routes and exposed features while effect-auth keeps the hard parts: authentication orchestration, public error mapping, operation security, continuations, and session-cookie commitment. It is the middle path described in [Architecture](/concepts/architecture/): more control than a preset, less security-critical code than calling primitives.

## Choose the seam [#choose-the-seam]

```text
Do the standard routes and client fit?
  yes -> mount CoreAuthHttpApiLive
  no  -> Is the authentication workflow still standard?
           yes -> bind *HttpOperations in your HttpApi
           no  -> compose primitives and own every skipped concern
```

Choose per endpoint. You can expose only password sign-in now and add account management later. Read [HTTP Operations](/concepts/http-operations/) before dropping to primitives.

| Concern                                    | effect-auth operation             | Your application                                                 |
| ------------------------------------------ | --------------------------------- | ---------------------------------------------------------------- |
| Authentication workflow and continuations  | Owns                              | Configures dependencies and UI                                   |
| Standard `AuthRateLimit` policy            | Executes once                     | Provides its implementation and rate-limit store                 |
| Domain-to-public error mapping and cookies | Owns                              | Declares a compatible endpoint contract                          |
| Decode failures and origin/CSRF protection | Does not travel with the function | Attaches and provides middleware                                 |
| Tenant, role, ownership, or step-up policy | Does not decide                   | Wraps the operation with [App Guards](/guides/app-owned-guards/) |
| Browser contract                           | Standard preset only              | Adds a typed extension for custom routes                         |

## Prefer app-owned named guards [#prefer-app-owned-named-guards]

For a copied application endpoint, bind policy next to the handler and keep provider infrastructure in Effect Context:

```ts
import { Duration, Effect } from "effect";
import { BotProtection } from "@effect-auth/core/AbuseProtection";
import { AuthRateLimit } from "@effect-auth/core/AuthRateLimit";
import {
  EmailGuards,
  IdentityGuards,
  PasswordGuards,
  RequestSecurity,
} from "@effect-auth/core/HttpApi";
import {
  PasswordLogin,
  PasswordRegistration,
} from "@effect-auth/core/Password";

const guardSignIn = PasswordGuards.signIn.withPolicy({
  rateLimit: AuthRateLimit.rules([
    {
      id: "app.password.sign_in.ip",
      key: "ip",
      limit: 8,
      window: Duration.minutes(15),
    },
  ]),
  botProtection: BotProtection.verify({
    action: "password-sign-in",
    allowedHostnames: ["app.example.com"],
    outageMode: "fail-closed",
  }),
});

const guardResetStart = PasswordGuards.resetStart.withPolicy(
  RequestSecurity.noop()
);

const signUp = ({ payload, request }) =>
  Effect.gen(function* () {
    const guarded = yield* PasswordGuards.signUp({ payload, request });
    const registration = yield* PasswordRegistration;
    return yield* registration.signUp(guarded.input);
  });

const signIn = ({ payload, request }) =>
  Effect.gen(function* () {
    const guarded = yield* guardSignIn({ payload, request });
    const password = yield* PasswordLogin;
    return yield* password.signIn(guarded.input);
  });
```

`PasswordGuards.signUp` uses library defaults. `guardSignIn` replaces only sign-in policy, without matching a route or operation string. `guardResetStart` is an explicit endpoint-local opt-out; it skips request rate limiting and bot verification but does not disable schema validation or domain invariants. Provide `AuthRateLimitStandardLive()` and provider infrastructure globally. A configured `Verify` policy without `BotChallengeVerifier` fails safely.

The same endpoint-local model covers email verification, email OTP, combined email auth, magic links, and authenticated identity availability:

```ts
const guardOtpStart = EmailGuards.emailOtp.start.withPolicy({
  botProtection: BotProtection.verify({ action: "email-otp-start" }),
});

const guardedOtp = yield * guardOtpStart({ payload, request });
const availability = yield * IdentityGuards.availability({ payload, request });
```

Email verify guards expose the optional trusted-device cookie alongside their domain-ready input. Combined email start runs request security exactly once. `IdentityGuards.availability` validates the session before request security and derives the rate-limit subject from the validated session, never from a request-provided user id.

## Bind a minimal password group [#bind-a-minimal-password-group]

This complete binding reuses two exported endpoint contracts, so their payloads, success variants, and declared errors exactly match `PasswordHttpOperations`. Sign-in can return a successful continuation such as `RequiresMfa`; do not flatten it into an error. See the [Error Model](/concepts/error-model/).

```ts title="app-auth-api.ts"
import { AuthRateLimitStandardLive } from "@effect-auth/core/AuthRateLimit";
import {
  AuthOriginCheckMiddleware,
  AuthOriginCheckMiddlewareLive,
  AuthSchemaErrorMiddleware,
  AuthSchemaErrorMiddlewareLive,
} from "@effect-auth/core/HttpApi";
import {
  PasswordHttpOperations,
  PasswordHttpOperationsLive,
  passwordSignInEndpoint,
  passwordSignUpEndpoint,
} from "@effect-auth/core/HttpApi/Password";
import { Effect, Layer } from "effect";
import { HttpApi, HttpApiBuilder, HttpApiGroup } from "effect/unstable/httpapi";

class AppPasswordHttpApiGroup extends HttpApiGroup.make("password")
  .add(passwordSignInEndpoint)
  .add(passwordSignUpEndpoint)
  .prefix("/account/password")
  .middleware(AuthSchemaErrorMiddleware)
  .middleware(AuthOriginCheckMiddleware) {}

export class AppAuthApi extends HttpApi.make("AppAuthApi").add(
  AppPasswordHttpApiGroup
) {}

const AppPasswordHttpApiGroupLive = HttpApiBuilder.group(
  AppAuthApi,
  "password",
  Effect.fn("app.auth.password")(function* (handlers) {
    const password = yield* PasswordHttpOperations;
    return handlers
      .handle("signIn", password.signIn)
      .handle("signUp", password.signUp);
  })
);

const PasswordOperationsLive = PasswordHttpOperationsLive.pipe(
  Layer.provide(AuthRateLimitStandardLive()),
  Layer.provide(Layer.mergeAll(AppAuthServicesLive, AppRateLimitLive))
);

export const AppAuthApiLive = AppPasswordHttpApiGroupLive.pipe(
  Layer.provide(PasswordOperationsLive),
  Layer.provide(AuthSchemaErrorMiddlewareLive),
  Layer.provide(
    AuthOriginCheckMiddlewareLive({
      allowedOrigins: ["https://app.example.com"],
    })
  )
);
```

`AppAuthServicesLive` and `AppRateLimitLive` are application composition from your password/runtime setup; HTTP server serving is also intentionally omitted. `AuthRateLimitStandardLive()` is supplied when constructing the operation service and the operation invokes it once. Do not call `AuthRateLimit.require` again in the handler. Add only distinct application policy around a method, for example `password.change(request).pipe(Guard.require(requireAccountAdmin()), mapAuthGuardErrors)`, with the endpoint declaring those mapped errors.

`AuthSchemaErrorMiddlewareLive` turns decode failures into the documented `bad_request` shape. Origin middleware protects the cookie-capable boundary; configure allowed origins for your deployment rather than treating rate limiting as CSRF protection.

Keep the endpoint's declared errors aligned when adapting a request or wrapping an operation. Expected failures such as invalid credentials, policy denial, and rate limiting remain typed and safe for clients; unexpected storage, hashing, or session failures are sanitized by the operation. Map any new app-guard failure into a declared public error before returning it. Never expose repository causes merely because they are present in an Effect error channel.

Finally, changing `/auth/password/*` to `/account/password/*` means the standard browser protocol cannot discover these routes. Define an Effect `HttpApi` client extension with `defineAuthHttpApiExtension`, or implement a separate client, as described in [Browser Client](/clients/browser-client/#extensions-and-custom-endpoints). Server reuse does not imply client compatibility.

For custom authentication methods, do not accept `aal`, `amr`, or raw evidence from an HTTP request. Create `customEvidence` only after server-side verification and register its exact `policyId` plus positive `policyVersion` with `CustomEvidencePolicies`; an unregistered version fails closed. The policy maps trusted properties to a local tier, canonical custom AMR, and optional independent-second-factor status. Raw evidence and policy properties are server-only. Standard HTTP schemas and generated client types expose derived `aal`, `amr`, `authTime`, and `mfaVerifiedAt` summaries, not authentication events.

