Esc

    Elixir SDK

    Idiomatic Elixir client for InferaDB with Phoenix integration.

    Coming soon. The Elixir SDK is under active development. The API surface shown here is based on the Rust SDK and may change before release.

    Fault-tolerant, process-based client for InferaDB’s authorization APIs. Requires Elixir 1.16+ / OTP 26+. Integrates with Phoenix and any Plug-based application.

    Installation

    # mix.exs
    defp deps do
      [
        {:inferadb, "~> 0.1"}
      ]
    end
    
    mix deps.get
    

    Configuration

    # config/runtime.exs
    config :inferadb,
      url: System.fetch_env!("INFERADB_URL"),
      credentials: [
        client_id: System.fetch_env!("INFERADB_CLIENT_ID"),
        private_key_path: System.fetch_env!("INFERADB_KEY_PATH"),
        certificate_id: System.fetch_env!("INFERADB_CERT_ID")
      ],
      organization: System.fetch_env!("INFERADB_ORG"),
      vault: System.fetch_env!("INFERADB_VAULT")
    

    Add the client to your supervision tree:

    # lib/my_app/application.ex
    children = [
      {InferaDB, name: MyApp.InferaDB}
    ]
    

    Authentication

    Three authentication methods:

    Method Use Case Security
    Client Credentials (Ed25519 JWT) Service-to-service High
    Bearer Token User sessions, OAuth Medium
    API Key Testing, simple integrations Basic

    Via application config above, or explicitly:

    {:ok, client} = InferaDB.start_link(
      url: "https://engine.inferadb.com",
      credentials: [
        client_id: "my-client",
        private_key_path: "client.pem",
        certificate_id: "cert-id"
      ]
    )
    

    Bearer Token

    {:ok, client} = InferaDB.start_link(
      url: "https://engine.inferadb.com",
      token: System.fetch_env!("INFERADB_TOKEN")
    )
    

    Permission Checks

    vault =
      InferaDB.organization(MyApp.InferaDB, "my-org")
      |> InferaDB.vault("production")
    
    # Simple check — returns {:ok, true} or {:ok, false}
    {:ok, true} = InferaDB.check(vault, "user:alice", "can_edit", "document:readme")
    

    With ABAC Context

    {:ok, allowed} = InferaDB.check(vault, "user:alice", "can_view", "document:readme",
      context: %{ip_address: "10.0.0.1"}
    )
    

    Require — Returns :ok or :error

    case InferaDB.require(vault, "user:alice", "can_edit", "document:readme") do
      :ok -> # permitted
      {:error, :access_denied} -> # denied
      {:error, reason} -> # other error
    end
    

    Require! — Raises on Deny

    # Raises InferaDB.AccessDeniedError
    InferaDB.require!(vault, "user:alice", "can_edit", "document:readme")
    

    With Consistency Token

    {:ok, allowed} = InferaDB.check(vault, "user:alice", "can_view", "document:readme",
      at_least_as_fresh: revision_token
    )
    

    Batch Check

    {:ok, results} = InferaDB.check_batch(vault, [
      {"user:alice", "can_edit", "document:readme"},
      {"user:bob", "can_view", "document:readme"},
    ])
    
    InferaDB.all_allowed?(results) # => true/false
    

    Relationships

    Write

    # Returns {:ok, revision_token}
    {:ok, token} = InferaDB.write_relationship(vault,
      resource: "document:readme",
      relation: "editor",
      subject: "user:alice"
    )
    

    Batch Write

    {:ok, token} = InferaDB.write_relationships(vault, [
      %{resource: "document:readme", relation: "editor", subject: "user:alice"},
      %{resource: "document:readme", relation: "viewer", subject: "user:bob"},
    ])
    

    List

    {:ok, rels} = InferaDB.list_relationships(vault,
      resource: "document:readme"
    )
    

    Delete

    :ok = InferaDB.delete_relationships(vault,
      resource: "document:readme",
      relation: "viewer",
      subject: "user:bob"
    )
    

    Lookups

    # What resources can Alice view?
    {:ok, resources} = InferaDB.list_resources(vault, "user:alice",
      permission: "can_view",
      resource_type: "document"
    )
    
    # Who can edit this document?
    {:ok, subjects} = InferaDB.list_subjects(vault, "document:readme",
      permission: "can_edit"
    )
    

    Testing

    MockClient (Fastest)

    alias InferaDB.Testing.MockClient
    
    client = MockClient.new()
      |> MockClient.on_check("user:alice", "can_edit", "document:readme", :allow)
      |> MockClient.on_check("user:bob", "can_edit", "document:readme", :deny)
      |> MockClient.on_check_any_subject("can_view", "document:readme", :allow)
      |> MockClient.default_deny()
    
    MockClient.verify!(client) # asserts all expectations were met
    

    InMemoryClient (Full Policy Evaluation)

    alias InferaDB.Testing.InMemoryClient
    
    {:ok, client} = InMemoryClient.start_link(
      schema: """
      type document {
          relation viewer
          relation editor
          relation can_view = viewer | editor
      }
      """,
      data: [
        %{resource: "document:readme", relation: "editor", subject: "user:alice"},
        %{resource: "document:readme", relation: "viewer", subject: "user:bob"},
      ]
    )
    

    TestVault (Real Instance)

    alias InferaDB.Testing.TestVault
    
    {:ok, vault} = TestVault.create(org, schema: schema_ipl)
    # vault auto-cleans up when the process stops
    

    ExUnit Integration

    defmodule MyApp.DocumentAuthorizationTest do
      use ExUnit.Case, async: true
    
      alias InferaDB.Testing.InMemoryClient
    
      setup do
        {:ok, client} = InMemoryClient.start_link(
          schema: "...",
          data: [%{resource: "document:readme", relation: "editor", subject: "user:alice"}]
        )
    
        vault =
          InferaDB.organization(client, "test")
          |> InferaDB.vault("test")
        %{vault: vault}
      end
    
      test "Alice can edit", %{vault: vault} do
        assert {:ok, true} = InferaDB.check(vault, "user:alice", "can_edit", "document:readme")
      end
    
      test "Bob cannot edit", %{vault: vault} do
        assert {:ok, false} = InferaDB.check(vault, "user:bob", "can_edit", "document:readme")
      end
    end
    

    Error Handling

    Standard {:ok, result} / {:error, reason} tuples with bang variants that raise:

    case InferaDB.check(vault, "user:alice", "can_edit", "document:readme") do
      {:ok, true} -> :allowed
      {:ok, false} -> :denied
      {:error, %InferaDB.Error{kind: :rate_limited, retry_after: delay}} ->
        Process.sleep(delay)
        # retry
      {:error, %InferaDB.Error{kind: kind, request_id: id}} ->
        Logger.error("Authorization error: kind=#{kind} request_id=#{id}")
    end
    

    Error kinds: :unauthorized, :forbidden, :not_found, :rate_limited, :schema_violation, :unavailable, :timeout, :invalid_argument.

    Framework Integrations

    Phoenix Plug

    # lib/my_app_web/plugs/inferadb_authorize.ex
    defmodule MyAppWeb.Plugs.InferaDBAuthorize do
      import Plug.Conn
    
      def init(opts), do: opts
    
      def call(conn, opts) do
        permission = Keyword.fetch!(opts, :permission)
        resource_fn = Keyword.fetch!(opts, :resource)
    
        vault =
          InferaDB.organization(MyApp.InferaDB, "my-org")
          |> InferaDB.vault("production")
    
        user_id = conn.assigns.current_user.id
        resource = resource_fn.(conn)
    
        case InferaDB.require(vault, "user:#{user_id}", permission, resource) do
          :ok ->
            conn
    
          {:error, :access_denied} ->
            conn
            |> put_status(:forbidden)
            |> Phoenix.Controller.json(%{error: "Forbidden"})
            |> halt()
        end
      end
    end
    
    # lib/my_app_web/router.ex
    pipeline :authorized_document do
      plug MyAppWeb.Plugs.InferaDBAuthorize,
        permission: "can_view",
        resource: fn conn -> "document:#{conn.params["id"]}" end
    end
    
    scope "/documents", MyAppWeb do
      pipe_through [:browser, :authenticated, :authorized_document]
      get "/:id", DocumentController, :show
    end
    

    Phoenix LiveView

    defmodule MyAppWeb.DocumentLive.Show do
      use MyAppWeb, :live_view
    
      @impl true
      def mount(%{"id" => id}, _session, socket) do
        vault =
          InferaDB.organization(MyApp.InferaDB, "my-org")
          |> InferaDB.vault("production")
    
        user_id = socket.assigns.current_user.id
    
        case InferaDB.require(vault, "user:#{user_id}", "can_view", "document:#{id}") do
          :ok ->
            {:ok, assign(socket, :document, Documents.get!(id))}
    
          {:error, :access_denied} ->
            {:ok, socket |> put_flash(:error, "Not authorized") |> redirect(to: ~p"/")}
        end
      end
    end