Licet evaluates SpiceDB permission schemas (.zed files) inside your Elixir application, with relationships stored in your own Postgres database. This guide adds it to an existing Phoenix or Ecto application. It assumes you have a working Ecto.Repo backed by Postgres 15 or newer. By the end, you'll have a schema, the database tables, a running instance, and your first checks.

If the vocabulary is new to you, read Concepts first.

Install

Add Licet to your dependencies in mix.exs:

def deps do
  [
    {:licet, "~> 0.1"}
  ]
end

Then fetch it:

mix deps.get

Write a schema

The schema describes your object types, the relations between them, and how permissions are computed. Create priv/licet/schema.zed:

definition user {}

definition document {
  relation owner: user
  relation viewer: user | user:*
  permission edit = owner
  permission view = owner + viewer
}

A document has owners and viewers. Owners can edit. Owners and viewers can view. user:* lets a viewer relationship name every user, which is how a document is made public.

Check the file before going further:

mix run -e 'IO.inspect(Licet.Schema.parse(File.read!("priv/licet/schema.zed")))'

A mistake is reported with its line and column. Schema language describes everything a schema can contain.

Create the tables

Licet stores relationships, schema versions, and snapshots in three tables, inside a Postgres schema of their own (licet by default). Generate a migration for them and run it:

mix licet.gen.migration
mix ecto.migrate

The generator writes priv/repo/migrations/<timestamp>_install_licet.exs. Pass --prefix to use another Postgres schema name and --path to choose the file. The prefix must be a lowercase identifier: letters, digits, and underscores, starting with a letter or underscore.

Start an instance

Add Licet to your supervision tree after the repo:

children = [
  MyApp.Repo,
  {Licet,
   name: MyApp.Licet,
   repo: MyApp.Repo,
   schema: {:file, Application.app_dir(:my_app, "priv/licet/schema.zed")}},
  MyAppWeb.Endpoint
]

name identifies the instance. You pass it as the first argument of every Licet function. repo is the repo that holds the tables. schema is the schema to load at startup; when it differs from the stored one, Licet stores the new version, after checking that the change doesn't strand existing relationships.

The instance refuses to start if the migration hasn't run. All options are listed in the Licet module documentation.

Write relationships

A relationship is a fact such as "Ada owns document 1", written document:1#owner@user:ada. Write a batch with Licet.write/3:

{:ok, token} =
  Licet.write(MyApp.Licet, [
    {:create, "document:1#owner@user:ada"},
    {:create, "document:1#viewer@user:bob"}
  ])

All updates in a call succeed or fail together. The result is a consistency token for this write.

Check permissions

Licet.check(MyApp.Licet, "document:1", "edit", "user:ada", consistency: {:at_least, token})
#=> {:ok, :allowed}

Licet.check(MyApp.Licet, "document:1", "edit", "user:bob", consistency: {:at_least, token})
#=> {:ok, :denied}

Licet.check(MyApp.Licet, "document:1", "view", "user:bob", consistency: {:at_least, token})
#=> {:ok, :allowed}

The consistency option guarantees these checks see the write that produced token. Without it, a check reads a shared snapshot that is refreshed every 5 seconds by default, which is faster and cached but can briefly miss a recent write. Consistency explains how to choose.

Make the document public with a wildcard:

{:ok, token} = Licet.write(MyApp.Licet, [{:create, "document:1#viewer@user:*"}])

Licet.check(MyApp.Licet, "document:1", "view", "user:cara", consistency: {:at_least, token})
#=> {:ok, :allowed}

Write in a transaction

Licet writes join an open Repo.transaction/2. Create a record and its owner together, so neither exists without the other:

MyApp.Repo.transaction(fn ->
  document = MyApp.Repo.insert!(changeset)
  {:ok, _token} = Licet.write(MyApp.Licet, [{:create, "document:#{document.id}#owner@user:#{user.id}"}])
  document
end)

A check inside the transaction already sees the new relationship. If the transaction rolls back, the relationship is gone too. See Transactions.

Next steps