Skip to main content
Permify stores your authorization data in the database of your choice, which serves as the single source of truth for all permission checks. You write this data through the Permify API, and it is queried automatically when you call the Check API or any lookup endpoint. Authorization data in Permify takes two forms: relationships and attributes.

Relationships

Relationships are the core of Permify’s data model. They build up a collection of Access Control Lists (ACLs) called relational tuples. Each relational tuple represents a fact: user U has relation R to object O. The simplest form is:
entity # relation @ subject
For example:
TupleMeaning
organization:1#admin@user:3User 3 is an admin of organization 1
document:1#owner@user:1User 1 owns document 1
document:1#maintainer@organization:2#memberAll members of organization 2 are maintainers of document 1
The relation field in the subject object controls what the subject refers to:
  • Empty ("") — the subject is a direct individual user (e.g. user:123).
  • "..." — the subject is the entity itself without a sub-relation. Used when the subject type is not the user entity.
  • A named relation (e.g. "member") — the subject is a userset: everyone who holds that relation on the subject entity.

Attributes

For scenarios where relationships alone are not enough — such as geo-based restrictions, time-based access, or boolean flags — Permify supports attributes. Attributes attach typed values to entities:
subject $ attribute | value
Examples:
AttributeMeaning
account:1$balance|double:4000Account 1’s balance is 4000
post:546$is_restricted|boolean:truePost 546 is marked as restricted
user:122$regions|string[]:US,MEXUser 122 is associated with the US and Mexico regions

Writing authorization data

Relationships and attributes are created via the Write Data API at runtime — typically when a user action in your application changes the authorization state (e.g. a user creates a document, joins an organization, or is granted a role). All data must conform to your authorization schema.

Write Data API

POST /v1/tenants/{tenant_id}/data/write Consider this schema for the examples below:
entity user {}

entity organization {

    relation admin  @user
    relation member @user

}

entity document {

    relation owner      @user
    relation parent     @organization
    relation maintainer @user @organization#member

    action view   = owner or parent.member or maintainer or parent.admin
    action edit   = owner or maintainer or parent.admin
    action delete = owner or parent.admin
}

Example: document owner

When user:1 creates document:2, write the ownership tuple document:2#owner@user:1:
curl --location --request POST 'localhost:3476/v1/tenants/t1/data/write' \
--header 'Content-Type: application/json' \
--data-raw '{
  "metadata": {
    "schema_version": ""
  },
  "tuples": [
    {
      "entity": {
        "type": "document",
        "id": "2"
      },
      "relation": "owner",
      "subject": {
        "type": "user",
        "id": "1",
        "relation": ""
      }
    }
  ]
}'

Example: organization admin

Relational tuple: organization:1#admin@user:3 — User 3 is an admin of organization 1.
curl --location --request POST 'localhost:3476/v1/tenants/t1/data/write' \
--header 'Content-Type: application/json' \
--data-raw '{
  "metadata": {
    "schema_version": ""
  },
  "tuples": [
    {
      "entity": {
        "type": "organization",
        "id": "1"
      },
      "relation": "admin",
      "subject": {
        "type": "user",
        "id": "3",
        "relation": ""
      }
    }
  ]
}'

Example: parent organization

Relational tuple: document:1#parent@organization:1#... — Organization 1 is the parent of document 1.
curl --location --request POST 'localhost:3476/v1/tenants/t1/data/write' \
--header 'Content-Type: application/json' \
--data-raw '{
  "metadata": {
    "schema_version": ""
  },
  "tuples": [
    {
      "entity": {
        "type": "document",
        "id": "1"
      },
      "relation": "parent",
      "subject": {
        "type": "organization",
        "id": "1",
        "relation": "..."
      }
    }
  ]
}'

Example: group-based maintainer

Relational tuple: document:1#maintainer@organization:2#member — All members of organization 2 are maintainers of document 1.
curl --location --request POST 'localhost:3476/v1/tenants/t1/data/write' \
--header 'Content-Type: application/json' \
--data-raw '{
  "metadata": {
    "schema_version": ""
  },
  "tuples": [
    {
      "entity": {
        "type": "document",
        "id": "1"
      },
      "relation": "maintainer",
      "subject": {
        "type": "organization",
        "id": "2",
        "relation": "member"
      }
    }
  ]
}'

Snap tokens

Every Write Data response includes a snap_token:
{
  "snap_token": "FxHhb4CrLBc="
}
A snap token encodes the timestamp of a write operation. Pass it in subsequent check requests to guarantee that the permission engine reads data at least as fresh as that write — preventing stale reads in distributed deployments.
{
  "metadata": {
    "snap_token": "FxHhb4CrLBc="
  },
  ...
}
See Snap Tokens for more details.

Audit logs and history

Permify uses MVCC (Multi-Version Concurrency Control) to maintain a complete history of all permission data changes. This provides a built-in audit trail — you can review what changed, who changed it, and when.
  • Historical review — query past versions of your permission state.
  • Current state review — inspect the latest version of any permission setting.
  • Garbage collection — a built-in GC process cleans up old versions to keep storage optimized. Configure it via the database.garbage_collection settings in your config file.

Build docs developers (and LLMs) love