---
title: "Architecture"
url: "https://effect-auth.itsbroly.com/concepts/architecture/"
description: "Understand effect-auth's integration layers, boundaries, security ownership, and composition model."
---



effect-auth exposes one authentication implementation at three integration levels. The levels are not separate products and do not contain competing authentication logic. They are progressively lower assembly points around the same domain services.

```text
more convenience                                              more flexibility
less application ownership                            more application ownership

  Presets                 HTTP Operations                    Primitives
  ┌──────────────┐         ┌──────────────┐                  ┌──────────────┐
  │ routes       │         │ typed HTTP   │                  │ domain       │
  │ middleware   │ uses    │ operations   │ uses             │ services     │
  │ operations   ├────────▶│ + security   ├─────────────────▶│ + runtime    │
  │ client shape │         │ semantics    │                  │ capabilities │
  └──────────────┘         └──────────────┘                  └──────────────┘
```

Choose the highest level that gives your application the control it actually needs. Moving down adds ownership, not inherently better security or better architecture.

## The three layers [#the-three-layers]

| Level               | Convenience | Flexibility                             | Application owns                                                                                         | Library still owns                                                                                         |
| ------------------- | ----------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| **Presets**         | Highest     | Built-in contract                       | Runtime dependencies, configuration, enabled features, deployment                                        | Routes, endpoint binding, standard middleware, operation orchestration, typed errors, cookie commitment    |
| **HTTP Operations** | High        | Application-owned routes and middleware | Public API contract, endpoint selection, route guards, additional boundary policy, client contract       | Authentication orchestration, standard operation security, HTTP success/error semantics, cookie commitment |
| **Primitives**      | Lowest      | Full flow control                       | Orchestration, transport, authorization, security policy, error mapping, auditing, and session decisions | Focused domain capabilities and typed domain errors                                                        |

### Presets [#presets]

Use `CoreAuthHttpApiLive` when the built-in `/auth/*` API is suitable. It assembles the standard `HttpApi`, group handlers, operation layers, schema-error middleware, and origin-check configuration. The matching `createAuthClient` targets this standard contract.

Presets are an assembly layer, not a second implementation of sign-in:

```ts
import {
  AuthHttpApiConfigLive,
  CoreAuthHttpApiLive,
} from "@effect-auth/core/HttpApi";
import { AuthRateLimitStandardLive } from "@effect-auth/core/AuthRateLimit";
import { Layer } from "effect";
import { HttpServer } from "effect/unstable/http";

export const AuthLive = CoreAuthHttpApiLive.pipe(
  Layer.provide(AuthRateLimitStandardLive()),
  Layer.provide(AppAuthServicesLive),
  Layer.provide(
    AuthHttpApiConfigLive({
      originCheck: { allowedOrigins: ["https://app.example.com"] },
    })
  ),
  Layer.provide(HttpServer.layerServices)
);
```

### HTTP Operations [#http-operations]

Use operation services such as `PasswordHttpOperations` when the application must own its `HttpApi` contract but wants to retain the library's complete HTTP-facing behavior. An operation accepts the endpoint request shape and returns the same typed success and error channels as the built-in endpoint handler.

```ts
import {
  PasswordHttpOperations,
  PasswordHttpOperationsLive,
} from "@effect-auth/core/HttpApi/Password";
import { Effect, Layer } from "effect";
import { HttpApiBuilder } from "effect/unstable/httpapi";

export const AppPasswordHttpApiGroupLive = HttpApiBuilder.group(
  AppAuthApi,
  "password",
  Effect.fn("app.auth.password")(function* (handlers) {
    const password = yield* PasswordHttpOperations;
    return handlers.handle("signIn", password.signIn);
  })
).pipe(Layer.provide(PasswordHttpOperationsLive));
```

The application may reuse built-in endpoint definitions or define its own schemas and adapt them before calling an operation. It must also apply the middleware appropriate to its public boundary. See [Custom Auth API](/guides/custom-auth-api/).

### Primitives [#primitives]

Use services such as `PasswordLogin`, `PasswordPrimaryFactor`, `Sessions`, or storage services when authentication is part of an application-specific workflow that an HTTP operation does not model.

```ts
import { Email } from "@effect-auth/core/Identifiers";
import { PasswordLogin } from "@effect-auth/core/Password";
import { Effect, Redacted } from "effect";

const signIn = Effect.gen(function* () {
  const password = yield* PasswordLogin;
  return yield* password.signIn({
    email: Email("reader@example.com"),
    password: Redacted.make("correct horse battery staple"),
  });
});
```

This call has domain meaning, but no HTTP boundary meaning. It does not by itself perform an origin check, decode an untrusted request, map errors to an API, or guarantee that the caller applied rate limits. The application must provide those controls.

## One implementation, different assembly [#one-implementation-different-assembly]

The password preset illustrates the composition:

```text
CoreAuthHttpApiLive
  └─ CoreAuthPasswordGroupLive
       └─ handlers.handle("signIn", operations.signIn)
            └─ PasswordHttpOperationsLive
                 ├─ standard AuthRateLimit execution
                 ├─ HTTP/domain input and error mapping
                 ├─ session-cookie commitment
                 └─ PasswordLogin
                      └─ AuthFlow + password/storage/runtime services
```

`CoreAuthPasswordGroupLive` does not recreate sign-in. It obtains `PasswordHttpOperations` and binds `operations.signIn` to the endpoint. A custom API can bind that exact function elsewhere. A primitives integration calls `PasswordLogin` below the HTTP mapping and operation-security seam.

The practical comparison is therefore concise:

```ts
// Preset: the library binds all included groups.
const ServerLive = CoreAuthHttpApiLive;

// HTTP Operations: the application binds a library operation.
handlers.handle("signIn", passwordOperations.signIn);

// Primitives: the application designs and secures the flow.
yield * passwordLogin.signIn(domainInput);
```

For a complete worked example across all three levels, see [Authentication: Password](/authentication/password/).

## Choose per endpoint, not per application [#choose-per-endpoint-not-per-application]

The three levels are compositional choices, not an application-wide mode. One server can mount an unchanged feature preset, bind an HTTP operation to an application-owned endpoint, and implement another endpoint from primitives. Choose the highest useful level separately at each boundary.

For example, the same application can:

| Endpoint area                    | Integration level               | Reason                                                     |
| -------------------------------- | ------------------------------- | ---------------------------------------------------------- |
| Passkey registration and sign-in | `PasskeyHttpApiLive` preset     | The built-in contract fits unchanged                       |
| Password sign-in                 | `PasswordHttpOperations.signIn` | The application wants its own route and request schema     |
| Account recovery review          | Recovery and session primitives | The transition is part of an application-specific workflow |

The granularity is not identical at every level:

* **Presets mount contracts.** `CoreAuthHttpApiLive` mounts its complete core contract; focused presets such as `PasskeyHttpApiLive`, `TotpHttpApiLive`, and `StepUpHttpApiLive` mount a feature contract. They are not runtime switches for replacing one endpoint inside an already mounted contract.
* **Operation services are assembled per feature.** `PasswordHttpOperationsLive`, for example, constructs one service containing `signIn`, `signUp`, reset, set, and change operations. A custom group can bind any subset of those methods endpoint by endpoint, but the layer must satisfy the feature service's declared dependencies.
* **Primitives compose at call sites.** Any custom handler can call `PasswordLogin`, `Sessions`, or another domain service directly, including alongside calls to HTTP operations. For each direct primitive path, the application owns the HTTP, security, error, audit, and session consequences skipped by that call.

HTTP operations retain their standard operation security, typed error/success semantics, and cookie commitment wherever they are bound. Their original group middleware does not travel with the function, so a custom endpoint still owns request decoding, origin/CSRF handling, caller guards, and any application-specific boundary policy.

Do not mount two handlers for the same effective method and path and expect one to override the other. To replace a built-in endpoint, omit or rebuild the preset group that owns that route and bind its operation or primitives in your custom contract. Alternatively, expose the custom behavior at a distinct path.

This makes gradual adoption possible: start with presets, replace only the endpoints whose public contracts need to diverge with HTTP Operations, and drop to Primitives only for workflows that require different domain orchestration.

## Boundaries and ownership [#boundaries-and-ownership]

Authentication crosses four distinct boundaries. Keeping them explicit prevents transport concerns from leaking into domain code and prevents domain assumptions from being mistaken for request security.

| Boundary    | Responsibility                                                                             | Typical effect-auth types                                              |
| ----------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| **Domain**  | Users, credentials, challenges, auth flow, and sessions as business capabilities           | `PasswordLogin`, `PasswordManagement`, `AuthFlow`, `Sessions`          |
| **Runtime** | Concrete storage, cryptography, secrets, mail, clocks, and configuration                   | `Layer` implementations provided by the application                    |
| **HTTP**    | Decode requests, authenticate callers, run boundary policy, map errors, and commit cookies | endpoint schemas, middleware, `*HttpOperations`, `CoreAuthHttpApiLive` |
| **Client**  | Call a known HTTP contract and decode its public result                                    | `createAuthClient` for the standard preset contract                    |

The client boundary is not the domain boundary. If an application changes routes or public schemas at the HTTP Operations level, it owns the corresponding client contract. Likewise, a valid domain value does not prove that an HTTP caller is authorized to submit it.

## Security executes exactly once [#security-executes-exactly-once]

Standard operation security is part of the HTTP operation implementation. For example, `PasswordHttpOperationsLive` captures the configured `AuthRateLimit` service and its sign-in operation invokes the applicable policy before continuing into password authentication. Both the preset and a custom API call that same secured operation.

```text
request → boundary middleware/guards → secured HTTP operation → domain service
                                      └─ AuthRateLimit.require(...) once
```

Consequences:

* Providing `AuthRateLimitStandardLive()` to an operation layer configures its policy; it does not require callers to invoke that policy again.
* Binding an operation into the preset or a custom group does not duplicate its standard security.
* Do not wrap an HTTP operation in another copy of the same standard `AuthRateLimit` check. That can consume rate-limit budget twice or duplicate other policy effects.
* Calling primitives bypasses the operation seam, so the application must arrange the required security once at its own boundary.

Not every endpoint has the same policy. Authentication, session, challenge, and credential-management operations have different requirements. Treat the operation as the owner of its documented standard policy rather than adding a blanket duplicate around every route. See [Security Policies](/concepts/security-policies/).

## Boundary guards and business guards [#boundary-guards-and-business-guards]

These guard categories answer different questions and may both be necessary.

| Guard              | Question                                       | Examples                                                                                                              | Placement                                          |
| ------------------ | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
| **Boundary guard** | May this request attempt the operation?        | authenticated session, recent step-up, tenant route access, application-specific rate limit, origin/CSRF policy       | Middleware or around an HTTP operation             |
| **Business guard** | Is this state transition valid for the domain? | credential belongs to the user, factor is active, challenge is unexpired, password is correct, session can be revoked | Inside domain services and operation orchestration |

A boundary guard must not replace a business invariant. Conversely, a business invariant does not authenticate the incoming request. At the HTTP Operations level, compose application-specific guards around the operation while leaving its standard security intact:

```ts
return password
  .change(request)
  .pipe(
    Guard.requireAll(
      requireRecentStepUp(request.request),
      changePasswordRateLimit(request.request)
    ),
    mapAuthGuardErrors
  );
```

This adds application policy; it does not re-run the operation's standard policy. At the Primitives level, the same application boundary must additionally cover all controls that the skipped operation would have supplied.

## Layer dependency composition [#layer-dependency-composition]

Effect layers make ownership visible in types. Build from runtime capabilities upward, provide lower layers to higher layers, and expose only the final services needed by the server.

```text
application runtime
  storage + crypto + secrets + mail + configuration
                         │
                         ▼
AuthKernelLive + feature layers (for example PasswordDefaultLive())
                         │
                         ▼
domain services (PasswordLogin, Sessions, ...)
                         │
                         ▼
*HttpOperationsLive + AuthRateLimit
                         │
                         ▼
custom HttpApi groups or CoreAuthHttpApiLive
```

```ts
const AppAuthServicesLive = PasswordDefaultLive().pipe(
  Layer.provideMerge(AuthKernelLive),
  Layer.provideMerge(AppAuthRuntimeLive)
);

const AppOperationsLive = PasswordHttpOperationsLive.pipe(
  Layer.provide(AuthRateLimitStandardLive()),
  Layer.provide(AppAuthServicesLive)
);

const AppHttpLive = AppPasswordHttpApiGroupLive.pipe(
  Layer.provide(AppOperationsLive)
);
```

`Layer.provide` satisfies dependencies without automatically exposing the supplied layer's outputs. `Layer.provideMerge` satisfies dependencies and retains both outputs for later feature layers. Use the required-service type of the layer as the source of truth instead of relying on initialization order.

Optional feature services are discovered when an operation layer is constructed. Compose enabled features into the services supplied to that operation layer so the resulting operation has one stable implementation for both preset and custom bindings.

## Decision guide [#decision-guide]

Choose **Presets** when:

* The standard routes, schemas, middleware, and client contract fit.
* You want secure defaults with the smallest integration surface.
* Customization is primarily runtime configuration or enabling features.

Choose **HTTP Operations** when:

* Your application owns route names, public payloads, middleware, or API grouping.
* You need a subset of capabilities or application-specific boundary guards.
* You still want standard orchestration, security execution, typed HTTP errors, and cookie behavior.

Choose **Primitives** when:

* Authentication is one stage of a genuinely custom workflow.
* The application must decide when to create a session or return a continuation.
* Existing operations cannot represent the required domain behavior.
* The team is prepared to own transport security, authorization, auditing, error mapping, and session consequences.

Do not move down merely to change a URL, add middleware, or add a boundary guard; HTTP Operations already provide those seams. Do not call a primitive from an endpoint merely to avoid adapting a schema; doing so also takes ownership of behavior that the operation would otherwise preserve.

## Related guides [#related-guides]

* [Authentication: Password](/authentication/password/) for a worked comparison of all three levels
* [Custom Auth API](/guides/custom-auth-api/) for an application-owned contract
* [Security Policies](/concepts/security-policies/) for standard and operation-specific policy
* [Sessions](/concepts/sessions/) for validation, rotation, revocation, and cookie behavior

