Skip to content

Checks

A check is a per-(caller, resource) boolean predicate, expressed as an Effect so it can reach a repository, a bus, or anything else through the Effect environment.

type Check<Caller, Resource, E = never, R = never> = (
caller: Caller,
resource: Resource,
) => Effect.Effect<boolean, E, R>;

Returning boolean is what lets checks compose before the single lift to a denial happens at the hasPermissions boundary. A check that raised the host’s denial error itself could not be OR’d with another — the first one to run would have already failed the effect.

One denial, at one place, is also what keeps the error out of every intermediate signature.

import { Check } from "@effect-server-utils/authz";
Check.any(isOwner, isAdmin); // OR — short-circuits on the first true
Check.all(isOwner, notArchived); // AND — short-circuits on the first false

The empty cases are chosen deliberately:

  • Check.any() with no checks returns false — denying by default is the safer floor.
  • Check.all() with no checks returns true, vacuously — the same semantics as Array.every.

Both parameters stay open on Check itself, so you can compose checks that are not registered anywhere. The typed registry is what pins Caller to your configured caller and Resource to the resource’s registered type.

A check that inspects only the caller declares one parameter:

type CallerCheck<Caller, E = never, R = never> = (caller: Caller) => Effect.Effect<boolean, E, R>;
const isAdmin: CallerCheck<Caller> = (caller) => Effect.succeed(caller.roles.includes("admin"));

Declaring the narrower arity — rather than a Check<Caller, unknown> that ignores its second parameter — is what lets one such check serve both an unscoped resource, whose checks are handed no resource at all, and a scoped one. TypeScript accepts a fewer-parameter function wherever a more-parameter one is expected, never the reverse.

So isAdmin above can be registered against platform.administer and OR’d into todo.update in the same file.

const isOwner = (caller: Caller, todo: Todo) => Effect.succeed(todo.ownerId === caller.userId);
const isInSameOrg = (caller: Caller, todo: Todo) =>
Effect.gen(function* () {
const membership = yield* organizationAcl.membershipFor(caller.userId);
return membership.organizationId === todo.organizationId;
});

The second one reaches a store, and can therefore fail with the configured CheckFailure. That is expected: a transient outage during authorization is the caller’s problem to retry, and a check that died on it instead would report a retryable outage as an internal error.

Registered checks are typed with R = never:

type ResourceCheck<Resource> = (
caller: Caller,
resource: Resource,
) => Effect.Effect<boolean, CheckFailure, never>;

A module needing cross-module data closes over its own ACL port at the registration site rather than reaching a shared service through the environment. That is what keeps R = never, and what lets a policy unit test provide nothing at all:

export const makeTodoPolicies = (organizationAcl: OrganizationAcl) => ({
todo: {
update: Check.any(isOwner, isOrgAdmin(organizationAcl)),
},
});
Export What it is
Check.any(...checks) OR, short-circuits on the first true, empty ⇒ false
Check.all(...checks) AND, short-circuits on the first false, empty ⇒ true
Check<Caller, Resource, E, R> the open predicate type
CallerCheck<Caller, E, R> caller-only arity, usable in both positions
ResourceCheck<Resource> what a scoped resource’s registry entry must be
UnscopedCheck what an unscoped resource’s registry entry must be