Permit

A single source of truth for authorization in Elixir. Define rules as code, source conditions from Ecto/SQL, auto-generate queries, and plug authorization seamlessly into Phoenix, LiveView, and Absinthe.

Core authorization library
Ecto integration & query generation
Phoenix Controllers & LiveView
GraphQL with Absinthe

Cheatsheet

Quick reference for core scenarios across Permit, Ecto, Phoenix and Absinthe.

Defining rules – exact match
Permit

# lib/my_app/permissions.ex

defmodule MyApp.Permissions do
  use Permit.Permissions, actions_module: Permit.Phoenix.Actions

  def can(%{role: :admin}), do: permit() |> all(MyApp.Blog.Article)

  def can(%{id: user_id}) do
    permit()
    |> all(MyApp.Blog.Article, author_id: user_id)
    |> read(MyApp.Blog.Article)
  end

  def can(_), do: permit()
end
                  
Quick checks
Permit

# Does user have permission to read a specific article?
can(user) |> do?(:read, article)

# Does user have permission to create an Article in general?
can(user) |> do?(:create, MyApp.Blog.Article)

# Convenience predicates also exist for all defined actions:
can(user) |> create?(MyApp.Blog.Article)
                  
Defining rules – operators
Permit

# All supported operators (examples)

can(user) |> # ...

# Equality / inequality
read(MyApp.Blog.Article, state: {:==, :published})
read(MyApp.Blog.Article, state: {:eq, :published})
read(MyApp.Blog.Article, state: {:!=, :archived})
read(MyApp.Blog.Article, state: {:neq, :archived})

# Comparisons
read(MyApp.Blog.Article, views: {:>, 100})
read(MyApp.Blog.Article, views: {:gt, 100})
read(MyApp.Blog.Article, views: {:>=, 100})
read(MyApp.Blog.Article, views: {:ge, 100})
read(MyApp.Blog.Article, views: {:<, 100})
read(MyApp.Blog.Article, views: {:lt, 100})
read(MyApp.Blog.Article, views: {:<=, 100})
read(MyApp.Blog.Article, views: {:le, 100})

# Pattern operators: converted to regular expressions in Elixir,
# generating LIKE/ILIKE queries
read(MyApp.Blog.Article, name: {:like, "%Guide%"})
read(MyApp.Blog.Article, name: {:ilike, "%guide%"})
# LIKE/ILIKE with escape option for literal % and _
read(MyApp.Blog.Article, name: {:like, "%!!%!%%!_%", escape: "!"})

# Regular expressions (not convertible to Ecto queries)
read(MyApp.Blog.Article, name: {:=~, ~r/Guide|Tutorial/})
read(MyApp.Blog.Article, name: {:match, ~r/guides?\\d+/i})

# Membership
read(MyApp.Blog.Article, tag: {:in, ["elixir", "phoenix"]})

# Nil checks
read(MyApp.Blog.Article, deleted_at: nil)           # is_nil(deleted_at)
read(MyApp.Blog.Article, deleted_at: {:not, nil}) # not is_nil(deleted_at)

# Negation with operators
read(MyApp.Blog.Article, name: {{:not, :like}, "%admin%"})

                  
Associated record rules
Permit

# Match on associated records using nested maps.
# Example: allow updating an Article when its author's group matches user's group

def can(%{group_id: group_id}) do
  permit()
  |> update(MyApp.Blog.Article, author: [group_id: group_id])
end
                  
Queries generated by rules
Permit.Ecto

# Build an Ecto query that reflects your Permit rules
MyApp.Authorization.accessible_by!(user, :read, MyApp.Blog.Article)
Repo.all(q)

# Examples of generated queries:

can(user) |> read(MyApp.Blog.Article)
# Query equivalent to:
from a in Article

can(user) |> read(MyApp.Blog.Article, views: {:>=, 100})
# Query equivalent to:
from a in Article, where: a.views >= 100

can(user) |> read(MyApp.Blog.Article, author: [group_id: user.group_id])
# Query equivalent to:
from a in Article,
  join: u in assoc(a, :author),
  where: u.group_id == ^user.group_id
                  
Unconvertible operator → custom query function
Permit.Ecto

# Some operators (e.g. :match / :=~ on regex) are not convertible to SQL automatically.
# You can supply an explicit Ecto dynamic query alongside the runtime predicate.

defmodule MyApp.Permissions do
  use Permit.Ecto.Permissions, actions_module: Permit.Phoenix.Actions

  import Ecto.Query

  def can(_user) do
    permit()
    |> read(MyApp.Blog.Article, [
      # Provide both: runtime predicate and equivalent Ecto dynamic
      {fn _subject, article -> article.title =~ ~r/Guide|Tutorial/i end,
        fn _subject, _object -> dynamic([a], ilike(a.title, ^"%guide%") or ilike(a.title, ^"%tutorial%")) end}
    ])
  end
end
                  
Controller configuration (load-and-authorize)
Permit.Phoenix

# lib/my_app_web/controllers/article_controller.ex

defmodule MyAppWeb.ArticleController do
  use MyAppWeb, :controller
  use Permit.Phoenix.Controller,
    authorization_module: MyApp.Authorization,
    resource_module: MyApp.Blog.Article

  def show(conn, _params) do
    # conn.assigns[:loaded_resource]
    render(conn, :show)
  end

  def index(conn, _params) do
    # conn.assigns[:loaded_resources]
    render(conn, :index)
  end
end
                  
LiveView + router wiring
Permit.Phoenix

# lib/my_app_web/router.ex

defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  import Phoenix.LiveView.Router

  scope "/", MyAppWeb do
    pipe_through :browser

    resources "/articles", ArticleController

    live_session :authz, on_mount: Permit.Phoenix.LiveView.AuthorizeHook do
      live "/articles", ArticleLive, :index
      live "/articles/:id", ArticleLive, :show
    end
  end
end

# lib/my_app_web/live/article_live.ex

defmodule MyAppWeb.ArticleLive do
  use Phoenix.LiveView
  use Permit.Phoenix.LiveView,
    authorization_module: MyApp.Authorization,
    resource_module: MyApp.Blog.Article,
    use_stream?: true

  def fetch_subject(session), do: MyApp.Accounts.fetch_user(session)
end
                  
Use built-in Phoenix actions
Permit.Phoenix.Actions

# Default grouping for Phoenix controllers/LiveViews:
# :read   => [:index, :show]
# :create => [:new, :create]
# :update => [:edit, :update]
# :delete => [:delete]

defmodule MyApp.Permissions do
  use Permit.Permissions, actions_module: Permit.Phoenix.Actions
  # define rules using :read/:create/:update/:delete
end
                  
Router-derived actions
Permit.Phoenix.Actions

# Merge action names defined in your Phoenix router
# into the default grouping schema.

defmodule MyApp.Actions do
  use Permit.Phoenix.Actions, router: MyAppWeb.Router
end

defmodule MyApp.Permissions do
  use Permit.Permissions, actions_module: MyApp.Actions
end
                  
Custom grouping + router merge
Permit.Phoenix.Actions

# Override grouping_schema/0 and merge routing actions

defmodule MyApp.Actions do
  use Permit.Phoenix.Actions

  @impl true
  def grouping_schema do
    Permit.Phoenix.Actions.grouping_schema()
    |> Permit.Phoenix.Actions.merge_from_router(MyAppWeb.Router)
    |> Map.put(:publish, [:publish])
  end
end
                  
Runtime inference (controllers & LiveViews)
Permit.Phoenix

# Controllers infer from action_name(conn)
# LiveViews infer from assigns[:live_action]
                  
Core mapping and resolvers
Permit.Absinthe

# lib/my_app_web/schema.ex

defmodule MyAppWeb.Schema do
  use Absinthe.Schema
  use Permit.Absinthe, authorization_module: MyApp.Authorization

  object :article do
    permit schema: MyApp.Blog.Article
    field :id, :id
    field :title, :string
  end

  query do
    field :article, :article do
      arg :id, non_null(:id)
      # loads and checks permissions
      resolve &load_and_authorize/2
    end

    field :articles, list_of(:article) do
      # returns only accessible records
      resolve &load_and_authorize/2
    end
  end
end
                  
Directive form (schema-level visibility)
Permit.Absinthe

# Inside the schema module
@prototype_schema Permit.Absinthe.Schema.Prototype

query do
  field :articles_via_directive, list_of(:article), directives: [:load_and_authorize] do
    permit action: :read
    resolve fn _, _, %{context: %{loaded_resources: articles}} ->
      {:ok, articles}
    end
  end
end
                  
Dataloader (batched loads + authorization)
Permit.Absinthe

# Object mapping with authorized dataloader
object :article do
  permit schema: MyApp.Blog.Article
  field :id, :id
  field :title, :string
  field :comments, list_of(:comment), resolve: &authorized_dataloader/3
end

# Add plugins for dataloader
@impl true
def plugins do
  [Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
end

# When using dataloader, set up context for fields that need it
field :article, :article do
  permit action: :read
  middleware Permit.Absinthe.Middleware.DataloaderSetup
  arg :id, non_null(:id)
  resolve &load_and_authorize/2
end
                  
Authorize mutation via middleware
Permit.Absinthe

# lib/my_app_web/schema.ex

mutation do
  @desc "Update an article"
  field :update_article, :article do
    permit action: :update

    arg :id, non_null(:id)
    arg :title, non_null(:string)
    arg :content, non_null(:string)

    # Loads the resource into context and authorizes access
    middleware Permit.Absinthe.Middleware.LoadAndAuthorize

    resolve fn _, %{title: title, content: content}, %{context: %{loaded_resource: article}} ->
      case MyApp.Blog.update_article(article, %{title: title, content: content}) do
        {:ok, article} -> {:ok, article}
        {:error, _changeset} -> {:error, "Could not update article"}
      end
    end
  end
end
                  

Articles & videos

Article
Future directions for Permit: Phoenix Scopes, Absinthe support, and advanced policy analysis | Curiosum
Future directions for Permit: Phoenix Scopes, Absinthe support, and advanced policy analysis | Curiosum
Explore planned Permit updates including Absinthe integration, Phoenix scopes, policy visualization tools, and static rule validation enhancements.
Article
Permit Authorization Library Updates and GraphQL Integration Debut  | Curiosum
Permit Authorization Library Updates and GraphQL Integration Debut | Curiosum
Explore Permit 0.3 updates, Phoenix LiveView support, and the new Absinthe integration for GraphQL APIs presented at ElixirConf EU 2025.
Article
Authorize access to your Phoenix app with Permit | Curiosum
Authorize access to your Phoenix app with Permit | Curiosum
The web app that doesn&#39;t need to manage resource authorization? Curiosum’s Permit library helps you if your stack is Phoenix, LiveView and Ecto.
Article
Discover Permit: A Unified Authorization Library for Elixir with Michał Buszkiewicz | Elixir Meetup #8 | Curiosum
Discover Permit: A Unified Authorization Library for Elixir with Michał Buszkiewicz | Elixir Meetup #8 | Curiosum
Explore Permit, a new authorization library for Elixir. Learn about its features, implementation, and future plans for enhancing authorization in Elixir apps.
Video
Elixir Meetup #8 Curiosum ▶ Michał Buszkiewicz ▶ Permit -An Uniform Authorization Library for Elixir
Elixir Meetup #8 Curiosum ▶ Michał Buszkiewicz ▶ Permit -An Uniform Authorization Library for Elixir
8th Elixir Meetup hosted by Curiosum ▶ http://curiosum.comABOUT SPEAKER: Michał Buszkiewicz [https://www.linkedin.com/in/michal-buszkiewicz/] ▶ Expert in Eli...
Video
Elixir Meetup #5 ▶ hosted by Curiosum ▶ Miguel Cobá &amp; Michał Buszkiewicz
Elixir Meetup #5 ▶ hosted by Curiosum ▶ Miguel Cobá &amp; Michał Buszkiewicz
5th Elixir Meetup hosted by Curiosum ▶ http://curiosum.comWe discussed 2 presentations of our Elixir experts:* Miguel Cobá: Elixir, Kubernetes and minikube* ...

Roadmap

  • Compile-time optimizations & caching

    Speed up permission checks with precomputed rule structures and caching strategies.

  • Static analysis for authorization rules

    Detect unreachable rules, overlaps and missing cases right in CI.

  • Policy playground & visualization

    Experiment with subjects/resources and see rule evaluation paths visually.

  • Code generators for Ecto patterns

    Scaffold common query conditions and record-loading strategies quickly.

  • Extended framework support

    First-class adapters for Ash and Commanded beyond Phoenix/Absinthe.

  • Route-based authorization helpers

    Simplify wiring by inferring actions straight from Phoenix routes.

  • Field-level authorization (research)

    Explore feasibility of safely constraining access to individual fields.

  • PostgreSQL RLS alignment (research)

    Investigate bridging Permit’s rules with database row-level security.

Permit's development depends on you.

We invite you to discuss, contribute and share Permit with others.

Support Permit

Slack channel

Join #permit to ask questions and share feedback.

GitHub Discussions

Open-ended conversations about features and usage.

GitHub Issues

Open and track issues in each repository.

Contributing guide

How to set up, code and submit contributions.

Join our Discord server

Hang out with the Curiosum community.

Spread the word

Share Permit with your network.