Permit.Absinthe 0.2: From Proof-of-Concept to Production-Ready GraphQL Authorization


When we announced Permit.Absinthe last June alongside the Permit 0.3 and Permit.Phoenix 0.3 releases, we described it as a working proof-of-concept - something you could pull from GitHub and play around with for simple APIs. We also shared a pretty honest TODO list of things that were still missing before the library could be considered a real tool for real applications.
With the release of Permit.Absinthe v0.2.0, I'm happy to say that we've checked off nearly every item on that list. This is the release where Permit.Absinthe exits the proof-of-concept phase and becomes something you can meaningfully use in production Absinthe APIs.
A quick refresher: what Permit.Absinthe does
If you're new here, Permit.Absinthe bridges the Permit authorization library with Absinthe GraphQL for Elixir. The core idea is the same load-and-authorize pattern familiar from Permit.Phoenix: you define who can do what in your permissions module, and Permit.Absinthe ensures that every GraphQL query and mutation respects those rules - loading only the records the current user is authorized to access, and rejecting unauthorized operations before your resolver logic even runs.
The setup looks like this - annotate your Absinthe schema with Permit metadata, and the library takes care of the rest:
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
field :content, :string
end
query do
field :article, :article do
arg :id, non_null(:id)
permit action: :read
resolve &load_and_authorize/2
end
field :articles, list_of(:article) do
permit action: :read
resolve &load_and_authorize/2
end
end
endThat much was already possible in v0.1. What's new is everything around it - the customizability, the Dataloader story, and the overall polish that makes this usable beyond toy examples.
What's new in v0.2.0
Customization options for real-world needs
In the previous blog post, I noted that one of the key gaps was the lack of options similar to those available in Permit.Phoenix - notably the ability to narrow down queries with a base query and to customize behaviour on authorization errors. This was at the top of the TODO list, and it's now fully addressed.
The permit macro now supports a rich set of options that cover the scenarios we kept running into when trying to use v0.1 in actual applications:
:base_query- provide a function that builds a custom Ecto base query before Permit scoping kicks in. This is the one you'll reach for when you need soft-delete filtering, tenant scoping, or any additionalWHEREclause that should apply regardless of permissions.:finalize_query- post-process the Ecto query after Permit scoping has been applied. Sorting, pagination, and similar concerns belong here.:fetch_subject- customize how the current user (or whatever your "subject" is) gets extracted from the resolution context. The default isresolution.context[:current_user], but if your auth setup is different, this is where you adapt it.:handle_unauthorizedand:handle_not_found- callbacks for when authorization fails or a resource can't be found. Instead of the default{:error, "Unauthorized"}tuple, you can return custom error structures, safe fallbacks, or whatever your API's error contract requires.:unauthorized_message- a simpler alternative when you just need a custom string message.:loader- swap out the default Ecto/Dataloader-based loading entirely. If your data comes from an external API, a cache, or some other non-Ecto source, you provide a function and Permit.Absinthe will use it.:wrap_authorized- a post-authorization hook to reshape or redact the data before it reaches the resolver.
Here's a taste of what this looks like in practice:
field :articles, list_of(:article) do
permit action: :read,
base_query: &non_deleted_articles/0,
finalize_query: &apply_pagination/2,
handle_unauthorized: &custom_error_response/1
resolve &load_and_authorize/2
endOne important caveat worth noting: because Permit.Absinthe captures callback functions passed to permit as AST at compile time (to avoid Absinthe boilerplate), functions referenced this way must be public - use def, not defp. References, function calls, and aliases are all supported.
Authorized Dataloader integration
This was the second major item on the TODO list, and a quite important one for smooth usage. Most Ecto-backed Absinthe APIs use Dataloader to batch-load associated resources, and we added this in v0.1 to ensure those loaded associations respected your authorization rules. This is particularly important for has_many associations, where you need to filter records that the current user shouldn't see.
It provided authorized_dataloader/3 - a resolver function that keeps Dataloader's batching efficiency while applying Permit authorization in the same flow. This worked well, but required an additional piece of boilerplate: a middleware had to be plugged into the parent's type to make it work.
v0.2 upgrades the authorized_dataloader/3 resolver so it doesn't require any additional boilerplate. Now all you need - having configured action and type mappings - is to use the authorized_dataloader/3 middleware to resolve the related field.
object :item do
permit schema: MyApp.Item
field :subitems, list_of(:subitem) do
permit action: :read
resolve &authorized_dataloader/3
end
endThis replaces the old Permit.Absinthe.Middleware.DataloaderSetup from v0.1 entirely (just remove it from your code when upgrading). The new resolver function handles creating and managing the necessary Dataloader source structures within the Absinthe resolution, so you don't need to wire up any additional middleware. Just keep the standard Absinthe Dataloader plugin in your schema's plugins/0 callback and you're set.
Custom ID fields
A smaller but practically important addition: you can now specify custom ID field names and parameters for resource lookups. Not every resource uses a field called :id as its primary identifier, and now you can tell Permit.Absinthe about that:
field :article_by_slug, :article do
arg :slug, non_null(:string)
permit action: :read, id_param_name: :slug, id_struct_field_name: :slug
resolve &load_and_authorize/2
endChoosing the right integration point
With the resolver function, the middleware, the directive, and the Dataloader integration all available, we've added a reference table to the README to help you pick the right approach for each situation:
And of course, for cases where none of the built-in integration points fit, you can always fall back to vanilla Permit syntax in a custom resolver - can(user) |> do?(:read, resource) works exactly the same way it does in the rest of the Permit ecosystem.
What this means: exiting proof-of-concept
Looking back at the TODO list from the previous article, here's where we stand:
Options similar to Permit.Phoenix (base_query, error handling, etc.) - done, and then some. The customization surface is now comparable to what Permit.Phoenix offers.
Dataloader integration for authorized association loading - done. authorized_dataloader/3 handles this cleanly.
Schema-wide hydrator for auto-applying middleware to all fields - this is still being explored. We've experimented with a custom schema hydrator that prewalks the schema and hydrates every top-level field with the Permit.Absinthe directive, but it's not ready for inclusion yet. This remains an area of active research and we'd love to hear from the community about use cases and expectations here.
The bottom line is that Permit.Absinthe is now a library you can reach for when building a production Absinthe API that needs authorization. The core patterns work, the customization hooks are there, and the Dataloader integration means you don't have to choose between performance and correctness for nested data.
A note on API stability
This is still a 0.x release, and we want to be upfront about what that means. We're actively using these APIs in practice and examining how they hold up against real-world requirements. While we believe the current design is solid, we reserve the right to make breaking changes before 1.0 if practical experience reveals better approaches.
That said, the conceptual model - annotating types with schemas, fields with actions, and choosing between resolver/middleware/directive integration - is something we're confident in. If breaking changes happen, they're more likely to be in the details of specific option names or callback signatures than in the overall architecture.
Get involved
Permit.Absinthe has grown from a 30-minute proof-of-concept into a library with comprehensive documentation, and real-world applicability. But there's still plenty of room for it to grow, and we'd love your help with that.
Here's how you can get involved:
- Try it out - add
{:permit_absinthe, "~> 0.2.0"}to your mix.exs and see how it fits into your Absinthe API. The README has thorough documentation and examples for every integration pattern. - Report issues - if something doesn't work as expected or the documentation is unclear, please open an issue on GitHub.
- Join the conversation - the #permit channel on Elixir Slack is the best place for development discussion, questions, and feature ideas.
- Contribute - pull requests are welcome. Whether it's a bug fix, a new feature, or documentation improvements, we appreciate it all. Check the Contributing Guide for details.
The full Permit ecosystem:
- Permit (core library)
- Permit.Ecto (Ecto integration)
- Permit.Phoenix (Phoenix & LiveView)
- Permit.Absinthe (Absinthe GraphQL)
Thank you for reading, and happy authorizing!
Want to power your product with Elixir? We’ve got you covered.
Related posts
Dive deeper into this topic with these related posts
You might also like
Discover more content from this category
The ability to upload files is a key requirement for many todays web and mobile applications. In this tutorial, we will look at how we can accomplish file uploads to local storage and S3 server in Phoenix with the help of Waffle library.
Elixir and its ecosystem is gradually becoming the go-to platform for many web application developers who want both performance and productivity without a tradeoff.
Everyone learning a new programming language has probably been there. There’s a missing piece in the new programming language, probably a library.



