Updates to Permit and Permit.Phoenix, announcing Permit.Absinthe
Permit now supports Phoenix LiveView 1.0 and Streams, and a new integration for Absinthe-based GraphQL APIs is in the works. This article presents the latest changes, improvements, and upcoming plans across the Permit ecosystem.
On May 16, 2025, I had the opportunity to present the Permit authorization framework to the public at this year's ElixirConf EU in Kraków - in-person and virtual conference guests can already view the recording, and for the rest of you I'll be happy to share it when it's on YouTube.
Apart from explaining authorization models and paradigms and how the concept of Permit libraries allows implementing them, and introducing to the basics of usage, we spoke a bit about features worked on right now as well as plans for the future. Among these was the Absinthe integration for authorizing GraphQL APIS, and I'm happy to announce that - as noted in the conference talk - Permit.Absinthe is now in the works, and a working proof-of-concept is already there at GitHub.
In addition, as a follow-up, Permit and Permit.Phoenix have had feature upgrades to 0.3.0 each, while Permit.Ecto has been patched to 0.2.4 - which I'll be happy to elaborate on below 🙂
In the meantime, let me introduce the basics of Permit, and how it can make developers' lives easier dealing with authorization in Elixir codebases.
Updates in Permit.Phoenix 0.3
Permit.Phoenix has finally been updated to support Phoenix LiveView 1.0, while the other side of this version constraint was increased to >= 0.20.x. This was to accommodate the inclusion of supporting streams, which has been tested for Permit.Phoenix >= 0.20.x. See changelog.
Streams
Prior to version 0.3.0, Permit.Phoenix would always store loaded-and-authorized records in LiveView assigns
- under :loaded_resource
or :loaded_resource
depending on action plurality. In the example below, since Something
is the Ecto schema module associated with this LiveView, in mount
and handle_params
these records
defmodule MyApp.SomethingLive do
use Permit.Phoenix.LiveView,
authorization_module: MyApp.Authorization,
resource_module: Something
# Authorization on mounting:
#
@impl true
def mount(_params, _session, %{assigns: %{live_action: :index}} = socket) do
# assigns[:loaded_resources] contains records filtered by authorization rules
{:ok, socket}
end
@impl true
def mount(_params, _session, %{assigns: %{live_action: :show}} = socket) do
# assigns[:loaded_resource] contains record filtered by params[:id] and
# checked against authorization rules
{:ok, socket}
end
# Authorization on URL navigation:
#
@impl true
def handle_params(_params, _url, %{assigns: %{live_action: :index}} = socket) do
# assigns[:loaded_resources] contains records filtered by authorization rules
{:noreply, socket}
end
@impl true
def handle_params(_params, _url, %{assigns: %{live_action: :show}} = socket) do
# assigns[:loaded_resource] contains record filtered by params[:id] and
# checked against authorization rules
{:noreply, socket}
end
end
By the way, In addition, Permit.Phoenix allows authorizing LiveView events as described here - in which case resources are also similarly loaded and authorized, especially useful for authorizing actions typically performed as events - such as record delections.
Since 0.19, Phoenix LiveView supports streams, intended for managing large collections on the client without keeping resources on the server. Now that LiveView's APIs, including the stream feature, are stable, Permit.Phoenix allows developers to stream loaded-and-authorized resources instead of assigning them, in plural actions such as :index
.
defmodule MyApp.SomethingLive do
# When :use_stream? option is set to true, :index action will have
# :loaded_resources in streams instead of assigns
use Permit.Phoenix.LiveView,
authorization_module: MyApp.Authorization,
resource_module: Something,
use_stream?: true
# Alternatively, the option can be controlled via a function,
# allowing to pattern-match on socket - and then e.g. the :live_action
def use_stream?(%{assigns: %{live_action: live_action}} = _socket)
when live_action in [:index, :search] do
true
end
def use_stream?(_socket), do: false
end
As always, feedback on this addition is very welcome - this is just the beginning of Permit.Phoenix's support for streams, and especially for this reason this is an opt-in feature, which may change in the future.
Inference of actions from the Phoenix router
While previously all action names valid for Permit needed to be explicitly defined in the actions module, which contributed to duplication and was rather cumbersome, as of v0.3 - since Permit actions in practice often map to controller or live actions, developers can now specify that action names should be taken from the web app's router.
defmodule MyApp.Authorization.Actions do
use Permit.Phoenix.Actions, router: MyAppWeb.Router
end
This is a convenient way to make writing authorization rules easier, since when e.g. a :view
action is added somewhere in the router, it can be used in the authorization rules file right away.
While I'm aware this is not ideal from the standpoint of dependency hygiene, as it effectively makes a module in MyApp
dependent on the MyAppWeb
namespace, it's an arguably mild tradeoff that's not compulsory to opt into. You can still use the prior mechanism if you don't want to introduce this dependency:
defmodule MyApp.Authorization.Actions do
use Permit.Actions
@impl true
def grouping_schema do
Permit.Phoenix.Actions.grouping_schema()
|> Map.merge(%{
view: [:read]
})
end
end
As I'm aware this is not a perfect solution to this issue, I'm open to recommendations, suggestions and pull requests to the Permit and Permit.Phoenix repositories - since we're still at 0.x, we're not strongly convinced this will be part of a future stable 1.x API.
The choice of this particular method of gathering action names data is in large part due to the original implementation requiring action names to be explicitly defined for mapping and grouping purposes (e.g. the view: [:read]
mapping seen above means that if :read
permission is given, :view
action is also authorized). I believe this might be a major refactoring opportunity in the future.
Various fixes
A number of other fixes and patches have been introduced - refer to the changelog to learn more. The CI pipeline includes a variety of tool versions in the execution matrix - ensuring there is reasonable backwards compatibility, which should be good news for integrating Permit with existing projects.
Updates in Permit 0.3
Permit 0.3's changes are less spectacular: apart from a number of cleanups and fixes to implementation details and docs, there is one noteworthy breaking change.
Friendly can(user) |> do(action, resource)
API
In addition to the plug-in load-and-authorize pattern of Permit.Phoenix, Permit has always allowed directly asking whether the user can perform a certain action on a given resource. For instance, when building a UI with action links concerning a record in a table, you'd do it like:
<%= if can(@current_user) |> delete?(article) do %>
<:action :let={article}>
<.link href={~p"/articles/#{article}"} method="delete" data-confirm="Are you sure?">
Delete
</.link>
</:action>
<% end %>
Suppose, though, that there is an enumerable list of actions that can evaluate to something like [:view, :approve, :delete]
and you want to iterate over such a list to render action links authorized for the current user and this record.
Prior to v0.3, to ask for permission to a dynamically given action, you'd have to use can(user) |> Permit.verify_record(record, action)
, which felt rather clumsy because of the argument order, and was not properly documented.
Now, we've changed the argument order in this function and aliased it as do?/3
mixed in when doing use Permit
. Now, in a module importing YourApp.Authorization
, you can use can(user) |> do?(action, record)
, which reads much more easily.
<%= for action <- @record_actions do %>
<%= if can(@current_user) |> do?(action, article) do %>
<:action let={article}>
...
</:action>
<% end %>
<% end %>
Updates in Permit.Ecto 0.2.4
This is probably the least interesting part (changelog): a few documentation details were fixed in docs and README, mostly concerning the usage of accessible_by/4
to construct Ecto queries. Nothing particularly exciting nor very meaningful, but we still encourage you to bump it in your mixfiles.
Introducing Permit.Absinthe for GraphQL
I've had the idea of bringing Permit to realize the load-and-authorize pattern in GraphQL APIs ever since we refactored the original proof-of-concept implementation to decouple Permit's Permit.Resolver
behaviour (and its Permit.Ecto.Resolver
implementation) from anything Phoenix-specific. Loading a record (or a list thereof) for which a specific subject
is authorized to perform a specific action
on is mappable to almost every architecture you encounter, albeit in some cases it might require some additional annotations. Absinthe happens to be a good example here.
Let's explore the steps we've taken so far to build a working proof-of-concept implementation -available at GitHub: we'll explain the design choices taken so far and what should come next.
Step 1: Mapping types to schemas
Typically, a type definition in Absinthe looks like this:
defmodule BlogWeb.Schema.Types do
use Absinthe.Schema.Notation
object :article do
field :id, :id
field :name, :string
field :content, :string
end
end
Since we typically grant permissions based on Ecto schema modules, we need to annotate this object with an Ecto schema. How to do this?
I've always thought of Permit as a DSL-free library, because I believe there are only a few slots for DSL expertise in a programmer's brain and I don't want Permit to compete for those:
The first thing we wanted to do is just use the existing Absinthe DSL's meta
feature:
defmodule BlogWeb.Schema.Types do
use Absinthe.Schema.Notation
object :article do
meta permit: [schema: Blog.Content.Article]
# ...
end
end
Conceptually, this is all we need - let's leave it like this and go one step further and see if doing the meta
thing turns out good enough.
Step 2: Mapping query and mutation fields to Permit actions
The tricky part in GraphQL is that the operations we do in such APIs don't necessarily map to action names, especially in queries, where instead we think of declaratively specifying what we want to see as the result. It's rather unlikely for any developer to create a query field named :index
to list articles or items. Rather, the field would usually be named just :articles
.
In mutations, the names are usually a bit more descriptive, but still not easy to destructure, like :delete_articles
. This means that in both cases we need to provide an explicit mapping to Permit action names, whereas the Ecto schema will be read out from the type definition. We thought: let's do this similarly via meta
options:
defmodule BlogWeb.Schema do
use Absinthe.Schema
query do
field :articles, list_of(:article) do
meta permit: [action: :read]
# ...
end
end
end
So far so good. Is it good enough, though? We've got the action and the schema, we still need the resolver function and a way to retrieve our authorization configuration (the module that does use Permit
or use Permit.Ecto
).
This is where we encounter a dilemma, because forcing the developer to pass authorization_module: Blog.Authorization
in every single field would not be acceptable, and we can't use the meta
keyword at the top level of the schema.
Instead, we came up with a mixin injected via use Permit.Absinthe
that does two things:
- Stores
:authorization_module
in a module attribute of our Absinthe schema - Provides a
permit/1
macro that adds the given options as well as theauthorization_module
(read out from the module attribute) to the field'smeta
.
defmodule BlogWeb.Schema do
use Absinthe.Schema
use Permit.Absinthe, authorization_module: Blog.Authorization
query do
field :articles, list_of(:article) do
permit action: :read
# ...
end
end
end
This is in fact a slim addition on top of Absinthe's DSL, but it's trade-off we deemed acceptable: with a single keyword, we are able to provide all options that drive Permit's interpretation of a given field, while not having to repeat ourselves when it comes to the authorization configuration module.
Step 3: Drop-in resolver function and middleware
Resolution in Absinthe relies on resolver functions, so this was the first thing we wrote to facilitate the load-and-authorize pattern. What the load_and_authorize/2
resource function does (full source code) is call Permit's resolver (slight naming clash - in this case, it's the logic of retrieving records authorized for a user to perform an action on) and respond with {:ok, record}
or {:error, whatever}
.
query do
field :articles, list_of(:article) do
permit action: :read
resolve &load_and_authorize/2
end
end
Simple as that!
Note, however, that - especially in mutations - we obviously don't just want to load-and-authorize; we need to take a specific action before we resolve. For this purpose, we have added a middleware that puts :loaded_resource
or :loaded_resources
into the resolution's context
:
mutation do
field :update_article, :article do
permit action: :update
arg :id, non_null(:id)
arg :name, non_null(:string)
arg :content, non_null(:string)
middleware Permit.Absinthe.Middleware.LoadAndAuthorize
resolve(fn _, params, %{context: context} ->
case Blog.Content.update_article(context.loaded_resource, params) do
{:ok, article} -> {:ok, article}
{:eror, changeset} -> {:error, MyHelpers.changeset_to_errors(changeset)}
end
end)
end
end
This way, if the loading and authorization has succeeded in the middleware (which reuses resolver code), we can then manipulate it in a custom resolver. Otherwise, the resolver doesn't need to be concerned with producing an authorization error response anymore - just does its business.
Step 4: Introducing a directive
While the resolver and the middleware are already promising enough, after my ElixirConf EU presentation I was hinted by one of the attendees at looking at wiring it up with GraphQL directives.
The potential advantage is that directives are part of GraphQL specification, which means that generated schema SDL and introspection will reflect this and this will be visible to developers interacting with the API.
We define the directive in the Permit.Absinthe.Schema.Prototype
module and add this to our schemas via @prototype_schema
- I haven't had success with the underdocumented import_directives
macro, so this is what works for now.
defmodule BlogWeb.Schema do
use Absinthe.Schema
use Permit.Absinthe, authorization_module: Blog.Authorization
# Include the directive definition
@prototype_schema Permit.Absinthe.Schema.Prototype
query do
field :articles, list_of(:article), directives: [:load_and_authorize] do
permit action: :read
resolve(fn _parent, _args, %{context: context} = _resolution ->
{:ok, context.loaded_resources}
end)
end
end
end
end
The directive usage has equivalent effect to using the LoadAndAuthorize
middleware, but what's nice is that it's visible in the generated schema.
$ mix absinthe.schema.sdl --schema BlogWeb.Schema
(...)
type RootQueryType {
"Get all articles"
articles: [Article] @loadAndAuthorize
(...)
}
What's missing?
So this is where we are right now: we can plug Permit into our GraphQL schemas at the resolver, middleware or directive level, depending on needs. This is what I wanted to achieve in the proof-of-concept phase - it's something you can pull from GitHub and use for playing around with authorizing access in simple APIs.
Meanwhile, here's what is still on the TODO list:
- We're still working on implementing options similar to those available in Permit.Phoenix so far, notably the ability to narrow down queries with
:base_query
and to customize resolver behaviour on authorization errors. - We'll research possibilities of plugging in the load-and-authorize middleware or directives into all fields of a given Absinthe schema. So far we've experimented with doing a custom schema hydrator to prewalk the schema hydrating every top-level field with Permit.Absinthe's directive - it's still in the works.
- As most Ecto-backed Absinthe APIs also use Dataloader to load associated resources for an entity, this also introduces the need to authorize access to these - especially important for
has_many
associations, which we must filter in order to ensure correct data access. We'll need to think of how to annotate the mapping between loading a nested resource and names of Permit actions, and whether or not assume any defaults.
The two latter topics are very interesting, and I'll be excited to discuss this with you and accept community ideas!
Future plans
I've briefly mentioned this during my presentation at the conference - and I'll elaborate on it in a follow-up article coming soon. Each of these topics would be worth an essay and not just a quick mention. There are a lot of ideas for improving Permit and making it adaptable into even more diverse workflows:
Ideas for improvement
These ideas are pretty much thought-out and this is what we'd like to implement at various stages of the libraries' development.
- Phoenix 1.8 Scopes
- Policy playground & visualization
- Static code analysis
- Compile-time optimizations and caching
Open questions
Some of these ideas come from discussions I've had with the audience in my ElixirConf presentation - with others, I've had them in mind for a long time, and while there are no proof-of-concepts of them yet, these are interesting possibilities to explore.
- New framework integrations - Ash, Commanded
- Leveraging PostgreSQL row-level security (RLS)
- Field-level authorization
- Phoenix route-based authorization
We will share a more detailed explanation of all these ideas soon in the Curiosum blog. For now, if you've managed to get here - thank you for reading and, again, I encourage you to check out the Permit libraries, star the repositories and share your feedback!
- Permit: https://github.com/curiosum-dev/permit
- Permit.Ecto: https://github.com/curiosum-dev/permit_ecto
- Permit.Phoenix: https://github.com/curiosum-dev/permit_phoenix
- Permit.Absinthe: https://github.com/curiosum-dev/permit_absinthe
FAQ
What is Permit?
Permit is an open‑source authorization library for Elixir apps, with companion libraries to load and authorize resources in Phoenix & LiveView. Permit provides a plain‑Elixir syntax for defining authorization rules, while Permit.Ecto automatically converts them into Ecto queries. :contentReference[oaicite:1]{index=1}.
Which packages make up Permit?
Permit is currently split into three packages, while a fourth one is in the works:
- permit: core library for defining authorization
- permit_ecto: translates rules into Ecto query scopes
- permit_phoenix: integrates with Phoenix controllers and LiveView for record preloading and checks
- permit_absinthe: integrates with Absinthe type definitions and schemas to authorize access to GraphQL API fields. :contentReference[oaicite:2]{index=2}
How do you define authorization rules with Permit?
Rules are defined in a Permissions
module using use Permit.Ecto.Permissions
. Example:
def can(%{role: :admin}), do: permit() |> all(Article)
def can(%{id: id}), do: permit() |> all(Article, user_id: id) |> read(Article)
:contentReference[oaicite:3]{index=3}
How does Permit enforce these rules?
- For controllers:
use Permit.Phoenix.Controller
auto‑preloads and checks@loaded_resource(s)
- For LiveView:
use Permit.Phoenix.LiveView
withAuthorizeHook
auto‑handles authorization inmount/2
andhandle_params/3
:contentReference[oaicite:4]{index=4}
How do you perform authorization checks?
You can call functions like:
# Generate query based on permissions
MyApp.Authorization.accessible_by!(user, :update, Article)
# Ask for permission on a given resource
can(user) |> update?(article)
:contentReference[oaicite:6]{index=6}
What authorization models does Permit support?
Permit uses ABAC (attribute‑based), supporting RBAC/MAC patterns, letting rules depend on any aspect of user or resource :contentReference[oaicite:7]{index=7}.
Is Permit limited to Phoenix?
No. Though designed for Plug, Phoenix controllers, and LiveView, it’s extensible and potentially adaptable to other frameworks like GraphQL (Absinthe) or Ash :contentReference[oaicite:8]{index=8}.
What’s the status of documentation and community adoption?
Permit’s code and Hex packages are published (latest release v0.3.0 on June 3, 2025) with solid examples. Community contributions are welcome :contentReference[oaicite:9]{index=9}.
How can I contribute to Permit?
You can test it in your apps, file issues or pull requests on GitHub, or explore future ideas like field‑level authorization, or GraphQL integration, Postgres RLS and many others. :contentReference[oaicite:10]{index=10}.