> ## Documentation Index
> Fetch the complete documentation index at: https://openfga.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Object to Object Relationships

> Modeling relationships between objects (e.g. folder parent of a document)

export const WriteRequestViewer = ({relationshipTuples, deleteRelationshipTuples, authorizationModelId, skipSetup, conflictOptions, allowedLanguages}) => {
  const [runtime, setRuntime] = useState(null);
  const [loadError, setLoadError] = useState(null);
  const [selectedLanguage, setSelectedLanguage] = useState(null);
  useEffect(() => {
    if (window.openfgaViewer) {
      setLoadError(null);
      setRuntime(window.openfgaViewer);
      return;
    }
    let script = document.querySelector('script[src="/openfga-viewer.js"]');
    const loaded = () => {
      clearTimeout(timeout);
      if (window.openfgaViewer) {
        setLoadError(null);
        setRuntime(window.openfgaViewer);
      } else setLoadError('The OpenFGA example helper did not initialize. Reload this page to retry.');
    };
    const failed = () => {
      clearTimeout(timeout);
      setLoadError('Unable to load OpenFGA examples. Reload this page to retry.');
    };
    if (!script) {
      script = document.createElement('script');
      script.src = '/openfga-viewer.js';
    }
    script.addEventListener('load', loaded);
    script.addEventListener('error', failed);
    const timeout = setTimeout(() => {
      if (!window.openfgaViewer) failed();
    }, 10000);
    if (!script.isConnected) document.head.appendChild(script);
    return () => {
      clearTimeout(timeout);
      script.removeEventListener('load', loaded);
      script.removeEventListener('error', failed);
    };
  }, []);
  if (loadError) return <div className="openfga-code-viewer__status" role="alert">
        {loadError}
      </div>;
  if (!runtime) return <div className="openfga-code-viewer__status" role="status">
        Loading examples...
      </div>;
  const {languageLabels: LANG_LABEL, languageGrammars: LANG_CODE} = runtime;
  const langs = runtime.selectLanguages('WriteRequestViewer', allowedLanguages);
  const activeLang = langs.includes(selectedLanguage) ? selectedLanguage : langs[0];
  const setupLangs = langs.filter(runtime.hasSetup);
  const buildSetupCode = lang => runtime.buildSdkSetup(lang, 'WriteRequestViewer');
  const buildCode = lang => runtime.buildRequestCode(lang, 'WriteRequestViewer', {
    relationshipTuples,
    deleteRelationshipTuples,
    authorizationModelId,
    conflictOptions
  });
  return <div data-openfga-viewer="WriteRequestViewer">
      {!skipSetup && runtime.hasSetup(activeLang) && <Accordion title="Initialize the SDK">
          <p>
            Install the <a href="https://openfga.dev/docs/getting-started/install-sdk">SDK or CLI</a> and deploy an OpenFGA server. Set
            FGA_API_URL, FGA_STORE_ID, and optionally FGA_MODEL_ID. These examples use no authentication; see{' '}
            <a href="https://openfga.dev/docs/getting-started/setup-sdk-client">client setup</a> for authentication and runtime
            prerequisites. Go snippets run inside main; Python requests run inside an async function and the client must
            be closed afterward.
          </p>
          <CodeGroup key={setupLangs.join(',')} onChange={index => setSelectedLanguage(setupLangs[index])}>
            {setupLangs.map(lang => <code key={lang} className={`language-${LANG_CODE[lang]}`} language={LANG_CODE[lang]} filename={LANG_LABEL[lang]}>
                {buildSetupCode(lang)}
              </code>)}
          </CodeGroup>
        </Accordion>}
      <CodeGroup key={langs.join(',')} onChange={index => setSelectedLanguage(langs[index])}>
        {langs.map(lang => <code key={lang} className={`language-${LANG_CODE[lang]}`} language={LANG_CODE[lang]} filename={LANG_LABEL[lang]}>
            {buildCode(lang)}
          </code>)}
      </CodeGroup>
    </div>;
};

export const CheckRequestViewer = ({user, relation, object, allowed, contextualTuples, context, headers, authorizationModelId, skipSetup, allowedLanguages, consistency}) => {
  const [runtime, setRuntime] = useState(null);
  const [loadError, setLoadError] = useState(null);
  const [selectedLanguage, setSelectedLanguage] = useState(null);
  useEffect(() => {
    if (window.openfgaViewer) {
      setLoadError(null);
      setRuntime(window.openfgaViewer);
      return;
    }
    let script = document.querySelector('script[src="/openfga-viewer.js"]');
    const loaded = () => {
      clearTimeout(timeout);
      if (window.openfgaViewer) {
        setLoadError(null);
        setRuntime(window.openfgaViewer);
      } else setLoadError('The OpenFGA example helper did not initialize. Reload this page to retry.');
    };
    const failed = () => {
      clearTimeout(timeout);
      setLoadError('Unable to load OpenFGA examples. Reload this page to retry.');
    };
    if (!script) {
      script = document.createElement('script');
      script.src = '/openfga-viewer.js';
    }
    script.addEventListener('load', loaded);
    script.addEventListener('error', failed);
    const timeout = setTimeout(() => {
      if (!window.openfgaViewer) failed();
    }, 10000);
    if (!script.isConnected) document.head.appendChild(script);
    return () => {
      clearTimeout(timeout);
      script.removeEventListener('load', loaded);
      script.removeEventListener('error', failed);
    };
  }, []);
  if (loadError) return <div className="openfga-code-viewer__status" role="alert">
        {loadError}
      </div>;
  if (!runtime) return <div className="openfga-code-viewer__status" role="status">
        Loading examples...
      </div>;
  const {languageLabels: LANG_LABEL, languageGrammars: LANG_CODE} = runtime;
  const langs = runtime.selectLanguages('CheckRequestViewer', allowedLanguages);
  const activeLang = langs.includes(selectedLanguage) ? selectedLanguage : langs[0];
  const setupLangs = langs.filter(runtime.hasSetup);
  const buildSetupCode = lang => runtime.buildSdkSetup(lang, 'CheckRequestViewer');
  const buildCode = lang => runtime.buildRequestCode(lang, 'CheckRequestViewer', {
    user,
    relation,
    object,
    allowed,
    contextualTuples,
    context,
    headers,
    authorizationModelId,
    consistency
  });
  return <div data-openfga-viewer="CheckRequestViewer">
      {!skipSetup && runtime.hasSetup(activeLang) && <Accordion title="Initialize the SDK">
          <p>
            Install the <a href="https://openfga.dev/docs/getting-started/install-sdk">SDK or CLI</a> and deploy an OpenFGA server. Set
            FGA_API_URL, FGA_STORE_ID, and optionally FGA_MODEL_ID. These examples use no authentication; see{' '}
            <a href="https://openfga.dev/docs/getting-started/setup-sdk-client">client setup</a> for authentication and runtime
            prerequisites. Go snippets run inside main; Python requests run inside an async function and the client must
            be closed afterward.
          </p>
          <CodeGroup key={setupLangs.join(',')} onChange={index => setSelectedLanguage(setupLangs[index])}>
            {setupLangs.map(lang => <code key={lang} className={`language-${LANG_CODE[lang]}`} language={LANG_CODE[lang]} filename={LANG_LABEL[lang]}>
                {buildSetupCode(lang)}
              </code>)}
          </CodeGroup>
        </Accordion>}
      <CodeGroup key={langs.join(',')} onChange={index => setSelectedLanguage(langs[index])}>
        {langs.map(lang => <code key={lang} className={`language-${LANG_CODE[lang]}`} language={LANG_CODE[lang]} filename={LANG_LABEL[lang]}>
            {buildCode(lang)}
          </code>)}
      </CodeGroup>
    </div>;
};

export const AuthzModelSnippetViewer = ({configuration, syntaxesToShow = ['dsl', 'json'], skipVersion}) => {
  const [codegenReady, setCodegenReady] = useState(false);
  const [dslReady, setDslReady] = useState(false);
  const [activeTab, setActiveTab] = useState(syntaxesToShow[0] || 'dsl');
  useEffect(() => {
    if (window.fgaCodegen) {
      setCodegenReady(true);
      return;
    }
    let script = document.querySelector('script[src="/fga-codegen.js"]');
    const loaded = () => {
      if (window.fgaCodegen) setCodegenReady(true);
    };
    if (!script) {
      script = document.createElement('script');
      script.src = '/fga-codegen.js';
    }
    script.addEventListener('load', loaded);
    if (!script.isConnected) document.head.appendChild(script);
    return () => script.removeEventListener('load', loaded);
  }, []);
  useEffect(() => {
    if (window.openfgaDsl) {
      setDslReady(true);
      return;
    }
    let script = document.querySelector('script[src="/openfga-dsl-highlight.js"]');
    const loaded = () => {
      if (window.openfgaDsl) setDslReady(true);
    };
    if (!script) {
      script = document.createElement('script');
      script.src = '/openfga-dsl-highlight.js';
    }
    script.addEventListener('load', loaded);
    if (!script.isConnected) document.head.appendChild(script);
    return () => script.removeEventListener('load', loaded);
  }, []);
  if (!codegenReady) {
    return <div className="openfga-code-viewer__status" role="status">Loading model viewer...</div>;
  }
  const {transformer} = window.fgaCodegen;
  const getDsl = () => {
    try {
      const isTypeDefinition = !configuration.type_definitions && Boolean(configuration.type);
      const model = isTypeDefinition ? {
        schema_version: '1.1',
        type_definitions: [configuration]
      } : configuration;
      const dsl = transformer.transformJSONToDSL(model);
      return skipVersion || isTypeDefinition ? dsl.replace('model\n  schema 1.1\n', '') : dsl;
    } catch (e) {
      return `// error converting to DSL: ${e.message}`;
    }
  };
  const getJson = () => {
    const json = {
      ...configuration
    };
    delete json.id;
    if (skipVersion) delete json.schema_version;
    return JSON.stringify(json, null, 2);
  };
  const showTabs = syntaxesToShow.length > 1;
  const DSL_COLORS = dslReady && window.openfgaDsl ? window.openfgaDsl.colors : {};
  const THEME_STYLE = {
    '--openfga-dark-background': DSL_COLORS.background,
    '--openfga-dark-default': DSL_COLORS.default,
    '--openfga-dark-accent': DSL_COLORS.green
  };
  const renderDsl = () => {
    const code = getDsl().replace(/^\n/, '').replace(/\n$/, '');
    const hasError = code.startsWith('// error converting to DSL:');
    const tokens = hasError ? [{
      text: code,
      type: 'error'
    }] : dslReady && window.openfgaDsl ? window.openfgaDsl.tokenize(code) : [{
      text: code,
      type: 'default'
    }];
    return <div className="openfga-code-viewer__code" data-language="dsl" data-state={hasError ? 'error' : undefined} role={hasError ? 'alert' : undefined}>
        {tokens.map((t, i) => <span className="openfga-code-token" data-token={t.type || 'default'} key={i} style={{
      '--openfga-dark-token-color': t.color || DSL_COLORS.default
    }}>
            {t.text}
          </span>)}
      </div>;
  };
  const tokenizeJson = text => {
    const strRanges = [];
    const claims = [];
    let m;
    const strRe = /"(?:[^"\\]|\\.)*"/g;
    while (m = strRe.exec(text)) {
      const s = m.index, e = s + m[0].length;
      strRanges.push({
        s,
        e
      });
      const isKey = (/^\s*:/).test(text.slice(e));
      claims.push({
        s,
        e,
        type: isKey ? 'json-key' : 'json-string'
      });
    }
    const inStr = (s, e) => strRanges.some(r => s >= r.s && e <= r.e);
    const numRe = /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g;
    while (m = numRe.exec(text)) {
      const s = m.index, e = s + m[0].length;
      if (!inStr(s, e)) claims.push({
        s,
        e,
        type: 'json-number'
      });
    }
    const kwRe = /\b(true|false|null)\b/g;
    while (m = kwRe.exec(text)) {
      const s = m.index, e = s + m[0].length;
      if (!inStr(s, e)) claims.push({
        s,
        e,
        type: 'json-keyword'
      });
    }
    const puncRe = /[{}\[\]:,]/g;
    while (m = puncRe.exec(text)) {
      const s = m.index;
      if (!inStr(s, s + 1)) claims.push({
        s,
        e: s + 1,
        type: 'json-punctuation'
      });
    }
    claims.sort((a, b) => a.s - b.s);
    const tokens = [];
    let cursor = 0;
    for (const c of claims) {
      if (c.s > cursor) tokens.push({
        text: text.slice(cursor, c.s),
        type: 'default'
      });
      tokens.push({
        text: text.slice(c.s, c.e),
        type: c.type
      });
      cursor = c.e;
    }
    if (cursor < text.length) tokens.push({
      text: text.slice(cursor),
      type: 'default'
    });
    return tokens;
  };
  const renderJson = () => {
    const code = getJson();
    const tokens = tokenizeJson(code);
    return <div className="openfga-code-viewer__code" data-language="json">
        {tokens.map((t, i) => <span className="openfga-code-token" data-token={t.type} key={i}>{t.text}</span>)}
      </div>;
  };
  return <div className="openfga-code-viewer" style={THEME_STYLE}>
      {showTabs && <div className="openfga-code-viewer__tabs">
          {syntaxesToShow.map(fmt => <button aria-pressed={activeTab === fmt} className="openfga-code-viewer__tab" data-state={activeTab === fmt ? 'active' : 'inactive'} key={fmt} onClick={() => setActiveTab(fmt)}>
              {fmt.toUpperCase()}
            </button>)}
        </div>}
      {(!showTabs || activeTab === 'dsl') && syntaxesToShow.includes('dsl') && renderDsl()}
      {(!showTabs || activeTab === 'json') && syntaxesToShow.includes('json') && renderJson()}
    </div>;
};

In this guide you'll learn how to model your application with [objects](/docs/concepts#what-is-an-object) that are not specifically tied to a user. For example, a `folder` is a `parent` of a `document`.

<Note>
  **When to use**

  This design pattern is helpful in the case where there are relationships between different objects. With OpenFGA, so long as both objects are in a type defined in the [authorization model](/docs/concepts#what-is-an-authorization-model), relationship tuples can be added to indicate a relationship between them.

  For example:

  * `communities` can contain `channels`
  * `channels` can contain `posts`
  * `channels` can contain `threads`
  * `threads` can contain `posts`
  * `bookshelf` can have `books`
  * `trips` can have `bookings`
  * `account` can contain `transactions`
  * `buildings` can have `doors`
</Note>

## Before you start

To better follow this guide, make sure you're familiar with some [OpenFGA Concepts](/docs/concepts) and know how to develop the things listed below.

You will start with the *[authorization model](/docs/concepts#what-is-an-authorization-model)* below, it represents a `document` *[type](/docs/concepts#what-is-a-type)* that can have users **[related](/docs/concepts#what-is-a-relation)** as `editor`, and `folder` type that can have users related as `viewer`.

<Accordion title="Authorization model">
  <AuthzModelSnippetViewer
    configuration={{
schema_version: '1.1',
type_definitions: [
  {
    type: 'user',
  },
  {
    type: 'document',
    relations: {
      editor: {
        this: {},
      },
    },
    metadata: {
      relations: {
        editor: { directly_related_user_types: [{type: 'user'}] },
      },
    },
  },
  {
    type: 'folder',
    relations: {
      viewer: {
        this: {},
      },
    },
    metadata: {
      relations: {
        viewer: { directly_related_user_types: [{type: 'user'}] },
      },
    },
  },
],
}}
  />

  <hr />

  In addition, you will need to know the following:

  <h3 id="modeling-user-groups">Modeling user groups</h3>

  You need to know how to add users to groups and grant groups access to resources. [Learn more →](/docs/modeling/user-groups)

  <h3 id="-concepts">OpenFGA concepts</h3>

  * A **[Type](/docs/concepts#what-is-a-type)**: a class of objects that have similar characteristics
  * A **[User](/docs/concepts#what-is-a-user)**: an entity in the system that can be related to an object
  * A **[Relation](/docs/concepts#what-is-a-relation)**: is a string defined in the type definition of an authorization model that defines the possibility of a relationship between an object of the same type as the type definition and a user in the system
  * An **[Object](/docs/concepts#what-is-an-object)**: represents an entity in the system. Users' relationships to it can be define through relationship tuples and the authorization model
  * A **[Relationship Tuple](/docs/concepts#what-is-a-relationship-tuple)**: a grouping consisting of a user, a relation and an object stored in OpenFGA
</Accordion>

## Step by step

### 01. Create parent relations in document

To represent that a `folder` can be a `parent` of a `document`, we first need to modify our `document` [type definition](/docs/concepts#what-is-a-type-definition) to allow a `parent` [relation](/docs/concepts#what-is-a-relation).

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type_definitions: [
  {
    type: 'user',
  },
  {
    type: 'document',
    relations: {
      parent: {
        this: {},
      },
      editor: {
        this: {},
      },
    },
    metadata: {
      relations: {
        parent: { directly_related_user_types: [{type: 'folder'}] },
        editor: { directly_related_user_types: [{type: 'user'}] },
      },
    },
  },
  {
    type: 'folder',
    relations: {
      viewer: {
        this: {},
      },
    },
    metadata: {
      relations: {
        viewer: { directly_related_user_types: [{type: 'user'}] },
      },
    },
  },
],
}}
/>

### 02. Add Parent Relationship Tuples

Once the type definition is updated, we can now create the [relationship](/docs/concepts#what-is-a-relationship) between a `folder` as a `parent` of a `document`. To do this, we will create a new **[relationship tuple](/docs/concepts#what-is-a-relationship-tuple)** that describes: **folder:budgets** is a `parent` of **document:may\_budget.doc**. In OpenFGA, [users](/docs/concepts#what-is-a-user) in the relationship tuples can not only be IDs, but also other objects in the form of `type:object_id`.

<WriteRequestViewer
  relationshipTuples={[
{
  _description: 'The user in this case is another object where the type is `folder` and the object_id is `budgets`',
  user: 'folder:budgets',
  relation: 'parent',
  object: 'document:may_budget.doc',
},
]}
/>

### 03. Check that parent folders have permissions

Once that relationship tuple is added to OpenFGA, we can [check](/docs/concepts#what-is-a-check-request) if the relationship is valid by asking the following: **"is folder:budgets a parent of document:may\_budget.doc?"**

<CheckRequestViewer user={'folder:budgets'} relation={'parent'} object={'document:may_budget.doc'} allowed={true} />

It is important to note that the current authorization model does not imply inheritance of permissions. Even though **folder:budgets** is a `parent` of **document:may\_budget.doc**, **it does not inherit the `editor` relation from `parent` to `document`.** Meaning `editors` on **folder:budgets** are not `editors` on **document:may\_budget.doc**. Further configuration changes are needed to indicate that and will be tackled in a later guide.

<Warning>
  When creating relationship tuples for OpenFGA make sure to use unique ids for each object and user within your application domain. We are using first names and simple ids to just illustrate an easy-to-follow example.
</Warning>

## Advanced object to object relationships

Object to object can be used for more advanced use case, such as [entitlements](/docs/modeling/advanced/entitlements). An example use case is to allow subscribers to be entitled to different plans.

### 01. Create authorization model with object to object relationships

To do this, the authorization model will have two [types](/docs/concepts#what-is-a-type) - feature and plan.

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type_definitions: [
  {
    type: 'user',
  },
  {
    type: 'feature',
    relations: {
      associated_plan: {
        this: {},
      },
      access: {
        union: {
          child: [
            {
              this: {},
            },
            {
              tupleToUserset: {
                tupleset: {
                  relation: 'associated_plan',
                },
                computedUserset: {
                  relation: 'subscriber_member',
                },
              },
            },
          ],
        },
      },
    },
    metadata: {
      relations: {
        associated_plan: { directly_related_user_types: [{type: 'plan'}] },
        access: { directly_related_user_types: [{type: 'user'}] },
      },
    },
  },
  {
    type: 'plan',
    relations: {
      subscriber_member: {
        this: {},
      },
    },
    metadata: {
      relations: {
        subscriber_member: { directly_related_user_types: [{type: 'user'}] },
      },
    },
  },
],
}}
/>

Type `feature` has two relations, associated\_plan and access. Relation `associated_plan` allows associating plans with features while `access` defines who can access the feature. In our case, the access can be achieved either from

* [direct relationship](/docs/concepts#what-are-direct-and-implied-relationships) via [direct relationship type restrictions](/docs/configuration-language#direct-relationship-type-restrictions). or `this`
* object to object relationship where a user can access because it is a subscriber\_member of a particular plan AND that plan is associated with the feature.

Here, we define `plan` as the user of object `feature` with relationship `associated_plan` rather than defining `feature` as the user of object `plan` with relationship `feature`. The reason we choose the former is that we want to describe our system in the following [plain language](/docs/modeling/getting-started#write-it-in-plain-language):

<div className="openfga-modeling-example">
  * A user can access a feature in a plan if they are a subscriber member of a plan that is the associated plan of a feature.
</div>

This will give us a flow of user->organization->plan->feature and allows us to answer the question of whether user can access a feature rather than whether user is subscriber of a plan.

### 02. Adding relationship tuples

To realize the relationship, we will need to add the following relationship tuples.

<WriteRequestViewer
  relationshipTuples={[
{
  _description: 'make anne as subscriber_member for plan:advanced',
  user: 'user:anne',
  relation: 'subscriber_member',
  object: 'plan:advanced',
},
{
  _description: 'The advanced plan is associated with the data preview feature',
  user: 'plan:advanced',
  relation: 'associated_plan',
  object: 'feature:data_preview',
},
]}
/>

### 03. Check to see if access is allowed without direct relationship

To validate that the authorization model and relationship tuples are correct, we can ask the question:

<CheckRequestViewer user={'user:anne'} relation={'access'} object={'feature:data_preview'} allowed={true} />

We see that `anne` is allowed to `access` `feature:data_preview` without requiring direct relationship.

### 04. Disassociating plan from feature

At any point in time, `plan:advanced` may be disassociated from `feature:data_preview`.

<WriteRequestViewer
  deleteRelationshipTuples={[
{
  _description: 'Remove advanced plan from data preview feature',
  user: 'plan:advanced',
  relation: 'associated_plan',
  object: 'feature:data_preview',
},
]}
/>

When this is the case, `anne` will no longer have `access` to `feature:data_preview` even though she is still a `subscriber_member` of `plan:advanced`.

<CheckRequestViewer user={'user:anne'} relation={'access'} object={'feature:data_preview'} allowed={false} />

<CheckRequestViewer user={'user:anne'} relation={'subscriber_member'} object={'plan:advanced'} allowed={true} />

## Related Sections

Check the following sections for more on how object-to-object relationships can be used.

<CardGroup>
  <Card title="Advanced Modeling Patterns: Entitlements" href="/docs/modeling/advanced/entitlements">
    Learn how to model entitlement access patterns.
  </Card>

  <Card title="Modeling Parent-Child Relationships" href="/docs/modeling/parent-child">
    Learn how to model parent and child relationships.
  </Card>

  <Card title="Modeling User Groups" href="/docs/modeling/user-groups">
    Learn how to model user groups.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.