effect-auth

App-owned Guards

Compose application authorization around reusable authentication operations.

Authentication proves a caller identity. A trusted boundary selects CurrentPrincipal. Application authorization decides whether that principal may act on a tenant or resource. Keep these steps explicit: a valid session does not grant membership, a role, or ownership.

Authentication, application authorization, and domain invariants protect different boundaries

@effect-auth/core/Guard is a small Effect combinator, not an authorization language. Use Policy.requirePermission with the maintained global-or-exact grant store or a custom Permissions backend; keep ownership, membership, and resource invariants app-owned.

CheckPlacementTypical failure
Valid caller sessionHTTP middleware401 unauthenticated
Tenant membership and permissionAround the operation403, or deliberate safe 404
Recent AAL2Around the sensitive operation403 step_up_required
Resource still belongs to tenantIn the write transactionTyped domain conflict
Legal state transitionDomain service / transactionTyped domain error

A guard rejects work early, but mutable facts must also constrain the write. A preflight ownership lookup alone creates a time-of-check/time-of-use gap.

Compose guards

Guard.all runs Effects left-to-right and stops on the first failure. Guard.require and Guard.requireAll run guards before an operation. The requireTenantAccess, requireRole, requireDocumentOwner, and mapAppGuardErrors names below are app-owned pseudocode.

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

const editDocumentGuard = (documentId: string) =>
  Guard.all(
    requireTenantAccess("acme"),
    requireRole("editor"),
    requireDocumentOwner(documentId),
    StepUp.toGuard({
      aal: "aal2",
      maxAge: Duration.minutes(10),
    })
  );

const updateDocument = (request: UpdateDocumentRequest) =>
  documents
    .update(request)
    .pipe(
      Guard.require(editDocumentGuard(request.documentId)),
      mapAppGuardErrors
    );

Place cheap identity and tenant checks before database lookups. Deliberately map unauthorized ownership to 404 when disclosure policy requires it; do not depend on evaluation order to hide resources.

StepUp.toGuard evaluates session assurance and freshness; it does not verify a factor. A static policy requires CurrentSession and can fail with StepUpRequired or CustomEvidencePolicyError. Adaptive policies additionally require StepUpCapabilities and may fail with StepUpDenied or StepUpCapabilityError. The browser completes the step-up flow, then retries the action so authentication, authorization, and freshness are re-evaluated.

Wrap HTTP Operations

HTTP Operations retain standard authentication orchestration, operation-specific abuse policy, public auth errors, and cookie effects. Add application policy around the exact endpoint-shaped operation instead of recreating it:

import * as Guard from "@effect-auth/core/Guard";
import {
  PasswordHttpOperations,
  type PasswordChangeOperation,
} from "@effect-auth/core/HttpApi/Password";
import { Effect } from "effect";

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

  const change: PasswordChangeOperation = (request) =>
    password
      .change(request)
      .pipe(Guard.require(requireAccountAdmin()), mapAppGuardErrors);

  return change;
});

Use PasswordChangeOperation, not the browser client's payload type: an HTTP Operation receives the endpoint request shape, including decoded payload and server request context.

The outer guard runs before password.change. Middleware or the handler must therefore provide CurrentPrincipal and any required session context; a session resolved internally by the operation is not available to an earlier guard.

Provide AuthRateLimitStandardLive() when constructing the operation layer, but do not invoke the same AuthRateLimit again in the wrapper. Add only app policy or a distinct stricter action-specific limit.

Map errors once

mapAuthGuardErrors from @effect-auth/core/HttpApi maps standard rate-limit, step-up, policy-denial, and infrastructure guard failures to public auth errors. It cannot know your application errors; map those separately at the endpoint boundary.

Internal outcomePublic boundary
Missing or invalid authentication401 unauthenticated
Authorization denialSanitized 403, or deliberate 404
StepUpRequired403 step_up_required
Rate limit exceeded429
Policy backend or unexpected storage failureSanitized 500 internal_error

Log stable reason codes and identifiers, never credentials, cookies, tokens, hostile headers, or complete resource records. Preserve detailed step-up and policy failures only in trusted diagnostics.

If you call domain services directly, the application also owns origin/CSRF controls, abuse policy, auditing, error projection, and session/cookie consequences. Prefer HTTP Operations when only routes or app authorization differ.

On this page