<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Patt-tom McDonnell</title>
    <description>The latest articles on DEV Community by Patt-tom McDonnell (@pthm).</description>
    <link>https://dev.to/pthm</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F127050%2F283470c4-04a9-47bd-8c40-19895d8c8b8f.jpeg</url>
      <title>DEV Community: Patt-tom McDonnell</title>
      <link>https://dev.to/pthm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pthm"/>
    <language>en</language>
    <item>
      <title>Building Melange: Zanzibar‑Style Auth Without a Separate Service</title>
      <dc:creator>Patt-tom McDonnell</dc:creator>
      <pubDate>Tue, 13 Jan 2026 05:11:00 +0000</pubDate>
      <link>https://dev.to/pthm/building-melange-zanzibar-style-auth-without-a-separate-service-a8e</link>
      <guid>https://dev.to/pthm/building-melange-zanzibar-style-auth-without-a-separate-service-a8e</guid>
      <description>&lt;p&gt;Over the holidays, I finally had some time to breathe and dive into a few "itch-to-scratch" side projects. A fresh repo, a blank README, and the rapid-fire implementation of core features. But like clockwork, I hit the same wall I’ve hit in every project for the last decade.&lt;/p&gt;

&lt;p&gt;I had to deal with &lt;strong&gt;Authorization.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;p&gt;We often lump "Auth" into one bucket, for good reason; they're close cousins, but they are two very different beasts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication (Authn)&lt;/strong&gt; is about identity: &lt;em&gt;Who are you?&lt;/em&gt; it's usually one of the first problems you solve, I have built more authentication systems than I have built full apps or products. Most unfinished apps have a functioning authentication system. Today, this is essentially a solved problem.&lt;/p&gt;

&lt;p&gt;Whether you use a managed provider or a solid library, the path is well-trodden. You set up a login form, handle a session, maybe add 2FA, and you’re done.&lt;/p&gt;

&lt;p&gt;The interesting thing about Authn is that it doesn’t really grow with your app. Whether you have ten users or ten million, the "login" logic remains largely the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authorization
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Authorization (Authz)&lt;/strong&gt; is the opposite. It’s about permissions: &lt;em&gt;What are you allowed to do?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the beginning, Authz is deceptively simple. Let’s look at a standard GitHub-style model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Users&lt;/strong&gt; belong to &lt;strong&gt;Orgs&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repos&lt;/strong&gt; belong to &lt;strong&gt;Orgs&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Members&lt;/strong&gt; of an Org can &lt;strong&gt;contribute&lt;/strong&gt; to its Repos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In your database, that looks like five tables: &lt;code&gt;users&lt;/code&gt;, &lt;code&gt;orgs&lt;/code&gt;, &lt;code&gt;repos&lt;/code&gt;, and the join tables &lt;code&gt;org_users&lt;/code&gt; and &lt;code&gt;org_repos&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Imagine an API route to update a repository: &lt;code&gt;PUT /repos/:id&lt;/code&gt;. You have an authenticated user ID (&lt;code&gt;user_id = 'X'&lt;/code&gt;) and a repository ID (&lt;code&gt;repo_id = 'Y'&lt;/code&gt;). You need to ask: &lt;em&gt;Can User X modify Repository Y?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To answer that, you have to verify the chain of ownership:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which Org owns this Repo?&lt;/li&gt;
&lt;li&gt;Is the User a member of that Org?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In SQL, it starts out manageable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;repos&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_repos&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_users&lt;/span&gt; &lt;span class="n"&gt;ou&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ou&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Y'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;ou&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'X'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A relatively simple query that joins through the ownership chain. It works fine for five tables. But real-world requirements never stay simple.&lt;/p&gt;

&lt;h3&gt;
  
  
  Growing Pains
&lt;/h3&gt;

&lt;p&gt;You add &lt;strong&gt;Issues&lt;/strong&gt;. Issues belong to Repos. Now, to check if a user can edit an issue, you’re joining &lt;code&gt;issues&lt;/code&gt; -&amp;gt; &lt;code&gt;repos&lt;/code&gt; -&amp;gt; &lt;code&gt;org_repos&lt;/code&gt; -&amp;gt; &lt;code&gt;org_users&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What if an Org has &lt;strong&gt;Teams&lt;/strong&gt;? And Teams can be nested? Now you aren't just checking if a user is in an Org; you're checking if they are in a Team, or a Sub-Team, that has a specific role on a Repo.&lt;/p&gt;

&lt;p&gt;Suddenly, you are writing more code to &lt;em&gt;authorize&lt;/em&gt; the request than you are to perform the actual business logic. Your queries look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Checking permission via nested teams and direct grants&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;permissions&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_members&lt;/span&gt; &lt;span class="n"&gt;tm&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accessor&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'repo&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;123'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'user&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;abc'&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'user&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;abc'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'write'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;strong&gt;Relationship-Based Access Control (ReBAC)&lt;/strong&gt;, and doing it manually in SQL or in your application code can be cumbersome and error-prone.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Light at the End of the Tunnel
&lt;/h3&gt;

&lt;p&gt;There are a number of solutions to this problem, and a plethora of services you can run or buy that aim to provide a way of answering these questions for you.&lt;/p&gt;

&lt;p&gt;A large portion of these systems are based off a paper published by Google, &lt;a href="https://research.google/pubs/zanzibar-googles-consistent-global-authorization-system/" rel="noopener noreferrer"&gt;"Zanzibar: Google’s Consistent, Global Authorization System"&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;These systems treat your permissions as a graph. They ignore your database tables and look at "tuples", simple strings that define relationships:&lt;code&gt;user:alice is member of org:acme&lt;/code&gt;&lt;code&gt;repo:engine is child of org:acme&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A few notable examples of these Zanzibar or Zanzibar-like systems are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://openfga.dev/" rel="noopener noreferrer"&gt;OpenFGA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spicedb.com/" rel="noopener noreferrer"&gt;SpiceDB&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.getketo.com/" rel="noopener noreferrer"&gt;Keto&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the rest of this post I will be talking about &lt;strong&gt;OpenFGA&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;OpenFGA is an open-source implementation of Zanzibar (donated to the CNCF by Okta/Auth0).&lt;/p&gt;

&lt;p&gt;OpenFGA provides a Domain Specific Language (DSL) to model your authorization rules. But it has a high "Entry Fee." Because OpenFGA is a separate service, you face a &lt;strong&gt;Synchronization Problem&lt;/strong&gt; : every time you update your database, you must also update the OpenFGA tuples via an API call. If one fails, your permissions are out of sync.&lt;/p&gt;

&lt;h3&gt;
  
  
  RoverApp - Pure Postgres ReBAC
&lt;/h3&gt;

&lt;p&gt;I was searching for a middle ground when I found a &lt;a href="https://getrover.substack.com/p/how-we-rewrote-openfga-in-pure-postgres" rel="noopener noreferrer"&gt;brilliant post by the team at Rover&lt;/a&gt;. They loved the OpenFGA model but hated the sync overhead. Their solution was to implement OpenFGA in Postgres. &lt;strong&gt;&lt;a href="https://github.com/rover-app/pgfga" rel="noopener noreferrer"&gt;pgfga&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Rover’s approach was elegant in its simplicity:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Model Table:&lt;/strong&gt; They created an &lt;code&gt;authz_model&lt;/code&gt; table to store the OpenFGA schema directly in Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Tuples (The Secret Sauce):&lt;/strong&gt; Instead of a static table of tuples that you have to sync, they used &lt;strong&gt;Database Views&lt;/strong&gt;. They mapped their existing domain tables (like &lt;code&gt;org_users&lt;/code&gt;) into a view that "looks" like an OpenFGA tuple table. Melange reads tuples from a &lt;code&gt;melange_tuples&lt;/code&gt; view you define over your existing tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Generic Check:&lt;/strong&gt; They wrote a recursive PL/pgSQL function—&lt;code&gt;check_permission(user, relation, object)&lt;/code&gt;—that traverses these views to find a path between the user and the resource.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This was a game-changer. It meant you could use the power of OpenFGA's modeling while keeping everything "Always-In-Sync" within a single Postgres transaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Melange
&lt;/h3&gt;

&lt;p&gt;I took the Rover solution and ran with it, but I eventually hit a ceiling.&lt;/p&gt;

&lt;p&gt;The Rover implementation relies on a generalized, recursive function. That’s fine for simpler models, but once I pushed on &lt;strong&gt;wildcards&lt;/strong&gt; (public access) and &lt;strong&gt;tuple-to-userset (TTU)&lt;/strong&gt; chains, the generic interpreter became the hot path.&lt;/p&gt;

&lt;p&gt;I got nerd-sniped. I spent the holiday implementing the full spec, but I chose a different architecture: &lt;strong&gt;Melange is a compiler.&lt;/strong&gt; It reads your OpenFGA schema and generates specialized PostgreSQL functions tailored to your relationship graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;melange migrate&lt;/code&gt; parses your OpenFGA schema and generates relation-specific SQL functions (plus a dispatcher). Those functions query a &lt;code&gt;melange_tuples&lt;/code&gt; view you define over domain tables, with compile-time-inlined role closure and TTU traversal paths. Runtime checks are pure SQL, so there’s no external service or tuple sync.&lt;/p&gt;
&lt;h4&gt;
  
  
  1. Specialized Dispatching
&lt;/h4&gt;

&lt;p&gt;Here’s a tiny TTU model from the official test suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rego"&gt;&lt;code&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="n"&gt;folder&lt;/span&gt;
  &lt;span class="n"&gt;relations&lt;/span&gt;
    &lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="n"&gt;viewer&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;
  &lt;span class="n"&gt;relations&lt;/span&gt;
    &lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="n"&gt;viewer&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;viewer&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Melange generates a dedicated &lt;code&gt;check_document_viewer(...)&lt;/code&gt; function plus a dispatcher. The generated SQL includes an explicit TTU edge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Recursive access path via parent -&amp;gt; viewer&lt;/span&gt;
&lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;melange&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_tuples&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;object&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'document'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'parent'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_permission&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_internal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_subject&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_subject&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'viewer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_visited&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_key&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entry point stays small and fast: &lt;code&gt;check_permission(...)&lt;/code&gt; simply routes to &lt;code&gt;check_document_viewer(...)&lt;/code&gt;, &lt;code&gt;check_folder_viewer(...)&lt;/code&gt;, and so on. Because those functions are generated for &lt;em&gt;your&lt;/em&gt; schema, they follow the precomputed paths through the relationship graph. PostgreSQL can plan the joins and indexes ahead of time.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. List Queries Are First-Class
&lt;/h4&gt;

&lt;p&gt;Real apps need more than binary checks. You often need to populate UI lists with what a user can actually see. Melange generates set-returning functions for this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Object Filtering:&lt;/strong&gt; &lt;em&gt;"Which documents can User X view?"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subject Filtering:&lt;/strong&gt; &lt;em&gt;"Which users can view Document Y?"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under the hood, you get functions like &lt;code&gt;list_document_viewer_objects(...)&lt;/code&gt; and &lt;code&gt;list_document_viewer_subjects(...)&lt;/code&gt;, routed through &lt;code&gt;list_accessible_objects(...)&lt;/code&gt; and &lt;code&gt;list_accessible_subjects(...)&lt;/code&gt;. They’re joinable straight into your application queries. Recent versions also add cursor-based pagination for these list APIs.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Performance &amp;amp; Compatibility
&lt;/h4&gt;

&lt;p&gt;Moving from interpretation to compilation pays off.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low-latency checks:&lt;/strong&gt; Local benchmarks are often sub‑millisecond once the database cache is warm. Network latency between your app and your database is a bigger overhead than the authorization logic itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema 1.1 compatibility:&lt;/strong&gt; Melange is validated against the OpenFGA test suite and supports the full 1.1 executable spec (excluding conditions).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Melange is built for the "Middle Phase." Most of us aren't Google, and we don’t need a globally distributed auth cluster on Day 1. We need something flexible enough to grow, but simple enough to live with. If you truly need multi-region or cross-database authorization later, the standard OpenFGA schema keeps your migration path open.&lt;/p&gt;

&lt;p&gt;Melange gives you the modeling power of OpenFGA with the reliability and consistency of in-transaction SQL. It’s designed to carry you from your first user until the day you’re actually big enough to need a dedicated Zanzibar cluster.&lt;/p&gt;

&lt;p&gt;Melange is open source and available on &lt;strong&gt;&lt;a href="https://github.com/pthm/melange" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/strong&gt;, with docs at &lt;strong&gt;&lt;a href="https://melange.sh" rel="noopener noreferrer"&gt;melange.sh&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>security</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
