This guide walks you through setting up AshGrant beyond the basics covered in the README's Quick Start.

Module-Based Resolver (Production)

For production, extract the resolver to a module:

defmodule MyApp.PermissionResolver do
  @behaviour AshGrant.PermissionResolver

  @impl true
  def resolve(nil, _context), do: []

  @impl true
  def resolve(actor, _context) do
    # Load permissions from database
    actor
    |> MyApp.Accounts.get_user_permissions()
    |> Enum.map(& &1.permission_string)
  end
end

Then reference it in your resource:

ash_grant do
  resolver MyApp.PermissionResolver
  # ...
end

Explicit Policies (Full Control)

For more control, disable default_policies and define policies explicitly:

ash_grant do
  resolver MyApp.PermissionResolver
  # default_policies false (default)

  scope :always, true
  scope :own, expr(author_id == ^actor(:id))
end

policies do
  # Admin bypass
  bypass actor_attribute_equals(:role, :admin) do
    authorize_if always()
  end

  # Read actions: use filter_check (returns filtered results)
  policy action_type(:read) do
    authorize_if AshGrant.filter_check()
  end

  # Write actions: use check (returns true/false)
  policy action_type([:create, :update, :destroy]) do
    authorize_if AshGrant.check()
  end
end

Resolver Context

The context parameter passed to your resolver contains:

KeyTypeDescription
:actortermThe actor performing the action
:resourcemoduleThe Ash resource module
:actionAsh.Action.tThe action struct
:tenantterm | nilCurrent tenant (from query/changeset)
:changesetAsh.Changeset.t | nilFor write actions
:queryAsh.Query.t | nilFor read actions

Example usage:

defmodule MyApp.PermissionResolver do
  @behaviour AshGrant.PermissionResolver

  @impl true
  def resolve(actor, context) do
    base_permissions = get_role_permissions(actor)

    # Add instance permissions based on context
    case context do
      %{resource: MyApp.Document, action: %{name: :read}} ->
        shared_docs = get_shared_document_ids(actor)
        instance_perms = Enum.map(shared_docs, &"document:#{&1}:read:")
        base_permissions ++ instance_perms

      _ ->
        base_permissions
    end
  end
end

Domain-Level DSL

When multiple resources share the same resolver and scopes, define them once at the domain level instead of repeating the same ash_grant do block in every resource.

When to use:

  • 3+ resources in a domain share the same resolver and common scopes (:always, :own, etc.)
  • You want a single place to change the resolver or add a scope for all resources

When NOT to use:

  • Resources in a domain have very different resolvers or scope logic
  • You only have 1–2 resources in the domain

Setup

defmodule MyApp.Blog do
  use Ash.Domain,
    extensions: [AshGrant.Domain]

  ash_grant do
    resolver MyApp.PermissionResolver

    scope :always, true
    scope :own, expr(author_id == ^actor(:id))
  end

  resources do
    resource MyApp.Blog.Post
    resource MyApp.Blog.Comment
  end
end

Resources inherit the domain's resolver and scopes automatically:

defmodule MyApp.Blog.Post do
  use Ash.Resource,
    domain: MyApp.Blog,
    authorizers: [Ash.Policy.Authorizer],
    extensions: [AshGrant]

  ash_grant do
    default_policies true
    # No resolver needed — inherited from domain
    # :always and :own scopes inherited from domain
    scope :published, expr(status == :published)  # Add resource-specific scopes
  end

  # ...
end

Inheritance Rules

ConfigResource defines itDomain defines itResult
resolverYesYesResource wins
resolverNoYesDomain's resolver used
scope (same name)YesYesResource wins (override)
scopeNoYesDomain scope inherited

Resource scopes can inherit from domain-defined parent scopes:

# Domain defines :own scope
# Resource adds :own_draft that inherits from domain's :own
ash_grant do
  scope :own_draft, [:own], expr(status == :draft)
end

A compile error is raised if no resolver is found from either the resource or the domain.

Resolver Patterns

Permissions with Metadata

Return AshGrant.PermissionInput structs for enhanced debugging and explain/4:

defmodule MyApp.PermissionResolver do
  @behaviour AshGrant.PermissionResolver

  @impl true
  def resolve(actor, _context) do
    actor
    |> get_roles()
    |> Enum.flat_map(fn role ->
      Enum.map(role.permissions, fn perm ->
        %AshGrant.PermissionInput{
          string: perm,
          description: "From role permissions",
          source: "role:#{role.name}"
        }
      end)
    end)
  end
end

Custom Structs with Permissionable Protocol

Implement the AshGrant.Permissionable protocol for your custom structs:

defmodule MyApp.RolePermission do
  defstruct [:permission_string, :label, :role_name]
end

defimpl AshGrant.Permissionable, for: MyApp.RolePermission do
  def to_permission_input(%MyApp.RolePermission{} = rp) do
    %AshGrant.PermissionInput{
      string: rp.permission_string,
      description: rp.label,
      source: "role:#{rp.role_name}"
    }
  end
end

# Then just return your structs from the resolver
defmodule MyApp.PermissionResolver do
  @behaviour AshGrant.PermissionResolver

  @impl true
  def resolve(actor, _context) do
    MyApp.Accounts.get_role_permissions(actor)
  end
end

Tip: Relational Scopes

Once you're comfortable with the basics, AshGrant supports relationship-based scopes using exists() and dot-path references. These work for both read and write actions:

ash_grant do
  scope :team_member, expr(exists(team.memberships, user_id == ^actor(:id)))
  scope :same_center, expr(order.center_id == ^actor(:center_id))
end

For read actions, these compile to SQL (EXISTS subquery or JOIN). For simple write actions, AshGrant uses a DB query fallback that handles most cases automatically. For multi-hop authorization, composite inheritance, or scopes that wrap relationship references inside functions, prefer the argument-based pattern — see the Argument-Based Scope guide. The Scopes guide covers relational scopes in detail.