---
title: "App-owned Guards"
url: "https://effect-auth.itsbroly.com/guides/app-owned-guards/"
description: "Compose application policy around reusable authentication operations."
---



Authentication establishes **who is calling** and supplies a validated `CurrentSession` / `CurrentActor`. Authorization decides **whether that actor may perform this action**. Keep both explicit: possession of a valid session does not grant tenant membership, a role, or access to a resource.

effect-auth's `Guard` module does not invent an authorization language. A guard is an `Effect`; `Guard.all` runs guards in order, while `Guard.require` and `Guard.requireAll` run them before an operation. Your application defines tenant, role, and ownership checks using its own repositories and error types.

## Choose the right check [#choose-the-right-check]

| Question                                          | Kind                                        | Typical placement                              | Failure                |
| ------------------------------------------------- | ------------------------------------------- | ---------------------------------------------- | ---------------------- |
| Is there a valid caller session?                  | Authentication                              | HTTP middleware                                | `401 unauthenticated`  |
| May this actor enter tenant `T`?                  | Boundary authorization                      | Around the operation                           | `403 forbidden`        |
| Does the actor have the required role?            | Boundary authorization                      | Around the operation                           | `403 forbidden`        |
| Does this resource belong to the actor or tenant? | Boundary authorization and domain invariant | Guard, then rechecked in the write transaction | `403` or safe `404`    |
| Is AAL2 recent enough?                            | Boundary step-up                            | Around the sensitive operation                 | `403 step_up_required` |
| Is this state transition legal?                   | Domain invariant                            | Domain service / transaction                   | Typed domain error     |

A boundary guard answers whether this request may attempt an operation. It is useful for early rejection, but it must not be the only protection for mutable facts. For example, checking document ownership before calling `transferDocument` leaves a time-of-check/time-of-use gap; the domain write must constrain the update by document and owner/tenant, or verify both in the same transaction. Conversely, a database ownership constraint does not authenticate the HTTP caller.

```text
request
  -> decode + origin/CSRF middleware
  -> authenticate session (CurrentSession / CurrentActor)
  -> app guards: tenant -> role/permission -> ownership -> recent step-up
  -> secured HTTP Operation (its AuthRateLimit runs once)
  -> domain service (enforces invariants transactionally)
  -> map typed failures to the public HTTP contract
```

## Build app-owned guards [#build-app-owned-guards]

The following names marked `app-owned` are pseudocode, not effect-auth exports. Each returns an `Effect<void, AppAuthorizationError, ...>` and may read `CurrentActor`, `CurrentSession`, route context, or an application repository.

```ts
import * as Guard from "@effect-auth/core/Guard";
import * as StepUp from "@effect-auth/core/StepUp";
import { Duration } from "effect";

const updateDocumentGuard = (documentId: string) =>
  Guard.all(
    requireTenantAccess("tenant-1"), // app-owned pseudocode
    requireRole("editor"), // app-owned pseudocode
    requireDocumentOwner(documentId), // app-owned pseudocode
    StepUp.toGuard({
      aal: "aal2",
      maxAge: Duration.minutes(10),
    })
  );
```

`StepUp.toGuard` is a real export. It reads `CurrentSession` and fails with `StepUpRequired`; it does not verify a factor. The client completes a [step-up flow](/authentication/step-up/), then retries the protected action so every attempt re-authenticates, re-authorizes, and re-evaluates freshness.

Compose the guard around the smallest complete operation, not around only a preliminary lookup:

```ts
const updateDocument = (request: UpdateDocumentRequest) =>
  documents.update(request).pipe(
    Guard.require(updateDocumentGuard(request.documentId)),
    mapAppGuardErrors // app-owned mapping to this API's declared errors
  );
```

`Guard.requireAll(a, b)` is equivalent when guards are assembled at the call site. Guards execute left-to-right and stop at the first failure, so put cheap identity and tenant checks before repository lookups and step-up checks. Do not rely on that order to hide resources; deliberately map unauthorized ownership to `404` when your disclosure policy requires it.

## Wrap HTTP Operations, do not duplicate them [#wrap-http-operations-do-not-duplicate-them]

HTTP Operations retain authentication orchestration, typed HTTP semantics, cookie commitment, and their configured standard security. An application-owned endpoint can add policy around the real operation service:

```ts
import { PasswordHttpOperations } from "@effect-auth/core/HttpApi/Password";
import * as Guard from "@effect-auth/core/Guard";
import { Effect } from "effect";

const changePassword = Effect.gen(function* () {
  const password = yield* PasswordHttpOperations;

  return (request: PasswordChangeRequest) =>
    password.change(request).pipe(
      Guard.require(requireAccountAdmin()), // app-owned pseudocode
      mapAppGuardErrors
    );
});
```

Provide `AuthRateLimitStandardLive()` when constructing the operation layer, but **do not call the same `AuthRateLimit` policy again in the wrapper**. The operation already executes its operation-specific security; duplicating it can consume rate-limit budget twice or repeat other effects. The wrapper should add only application policy such as tenant access, business permissions, resource scope, or stricter action-specific abuse controls. See [Security Policies](/concepts/security-policies/) and [Architecture](/concepts/architecture/).

## Map failures at the boundary [#map-failures-at-the-boundary]

Keep guard errors typed until the HTTP boundary. Map missing/invalid authentication to `401`; authorization denial to a sanitized `403` (or deliberate `404`); `StepUpRequired` to `403 step_up_required`; rate limits to `429`; and unexpected repository failures to `500`. Log stable reason codes and identifiers, not cookies, credentials, hostile headers, or sensitive resource data. Preserve `StepUpRequired.failures` for trusted diagnostics rather than returning them wholesale.

If you call primitives such as `Sessions`, `PasswordLogin`, or storage services directly, the application owns request authentication, authorization, origin/CSRF controls, abuse policy, auditing, error mapping, and session/cookie consequences. `Sessions` validates, creates, rotates, revokes, and updates sessions; `StepUp` evaluates assurance/freshness; `Guard` only composes Effects; HTTP Operations add standard operation security and HTTP behavior. Prefer Operations when only routes or app guards differ. Drop to primitives only when the workflow itself differs. See [Custom Auth API](/guides/custom-auth-api/).

