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

# Sharing Memory Across Users and Teams

> Give every user their own dataset, then open one up — first to a single colleague with an ACL grant, then to an entire role inside a tenant

Two people ingest documents into the same cognee deployment, and neither should see the other's data by default — until one of them asks for access, and later a whole research team needs it. That is the sequence every multi-user deployment runs into the first time someone says "can you share that dataset with me?"

## What You'll Build

Three users, two documents, and one instance with access control on. `user_1` remembers a PDF into an `AI` dataset while `user_2` remembers a text passage into a `QUANTUM` dataset, and the script then works through what `user_1` can and cannot do with someone else's dataset: reads and writes are refused, a read grant from the owner turns the refusal into an answer, and a `CogneeLab` tenant with a `Researcher` role finally lets `user_3` read a tenant-owned dataset on the strength of role membership alone. Every refusal along the way is a real `PermissionDeniedError` caught and printed, so a run reads as a transcript of the permission system making decisions.

The complete runnable script is
[`examples/demos/permissions/user_permissions_and_access_control_example.py`](https://github.com/topoteretes/cognee/blob/main/examples/demos/permissions/user_permissions_and_access_control_example.py) —
this page walks through its key moments rather than reproducing it.

## Features in Play

* [Permission Snippets](/guides/permission-snippets) — the tenant and role calls in short copy-paste form, with the four permission names spelled out; it grants with the unchecked `give_permission_on_dataset` rather than the authorizing wrapper used here
* [ACL](/core-concepts/multi-user-mode/permissions-system/acl) — `authorized_give_permission_on_datasets` writes the rows that decide each read in this demo, and raises `PermissionDeniedError` when there is no row to match
* [Tenants](/core-concepts/multi-user-mode/permissions-system/tenants) — the `CogneeLab` organization, and the active-tenant context that decides which datasets a grant may target
* [Roles](/core-concepts/multi-user-mode/permissions-system/roles) — the `Researcher` role, so access is granted once and every member inherits it
* [Remember](/core-concepts/main-operations/remember) — each ingestion call runs as a `user` and makes that user the sole owner of the dataset it creates
* [Recall](/core-concepts/main-operations/recall) — every read names both a `user` and explicit `dataset_ids`, and returns results only when an ACL allows it

## What to Expect

The excerpts below come from one real run, trimmed of most log lines. Every ingestion and every recall is a live LLM call, so the wording of the answers varies from run to run; the permission decisions do not.

**The instance is emptied and each owner is registered in turn.** `Data reset complete.` follows the two prunes, `Relational migrations applied (target head).` is `setup()` rebuilding the user-management tables, and each `create_user` call prints the address it registers before the new principal's id is logged.

```text theme={null}
Resetting cognee data...
...
Data reset complete.
...
Relational migrations applied (target head).
Creating user_1: user_1@example.com
User 000f9b41-bf09-4f1d-89f8-18442c626c29 has registered.
...
Creating user_2: user_2@example.com
User 494df8a4-d085-4545-8644-87e36b5329a9 has registered.
```

**Reading your own dataset needs no grant.** `user_1` recalls from `ai_dataset_id` and a `graph_completion` result comes straight back, summarizing the PDF it ingested a moment earlier — the owner holds every permission on the dataset its `remember()` created.

```text theme={null}
Recall results as user_1 on dataset owned by user_1:
kind='graph_completion' search_type='GRAPH_COMPLETION' text='A multi-page overview of artificial intelligence: definitions and types (narrow/weak, AGI/ASI), machine learning and deep learning ...
```

**The two refusals are the default posture.** Pointing the same recall at `user_2`'s dataset raises `PermissionDeniedError`, and so does a `remember()` into it. The `403` lines are cognee logging the exception before the script catches it and prints its own line, so expect them on a healthy run — this is what ownership alone gets everyone else: nothing.

```text theme={null}
Recall result as user_1 on the dataset owned by user_2:
...
2026-09-11T17:27:27.876388 [error    ] PermissionDeniedError raised (Status code: 403) [cognee.shared.logging_utils]
2026-09-11T17:27:27.892307 [error    ] PermissionDeniedError raised (Status code: 403) [cognee.shared.logging_utils]
User: <cognee.modules.users.models.User.User object at 0x124759010> does not have permission to read from dataset: QUANTUM

Attempting to remember new data as user_1 to dataset owned by user_2:
User: <cognee.modules.users.models.User.User object at 0x124759010> does not have permission to write to dataset: QUANTUM
```

**One `"read"` grant turns the refusal into a result.** The recall that failed two steps earlier is replayed unchanged, and this time `recall: 1 results` is logged and a completion is returned instead of a 403. The ACL row is the only thing that changed between the two attempts.

```text theme={null}
Operation started as user_2 to give read permission to user_1 for the dataset owned by user_2

Recall result as user_1 on the dataset owned by user_2:
...
2026-09-11T17:27:44.407459 [info     ] recall: 1 results across sources=['graph'] (session=-) [recall]
kind='graph_completion' search_type='GRAPH_COMPLETION' ...
```

**The tenant is built in a forced order, and the first role grant is refused.** Each line is one constraint being satisfied: tenant, active tenant, role, new user, tenant membership, role membership, and finally `user_3` selecting `CogneeLab` as its own active tenant. Read the last two lines closely — `user_2` owns both `CogneeLab` and the `QUANTUM` dataset, but that dataset belongs to `user_2` personally, so it is out of scope for a grant made from inside the tenant.

```text theme={null}
User 2 is creating CogneeLab tenant/organization
User 2 is selecting CogneeLab tenant/organization as active tenant

User 2 is creating Researcher role

Creating user_3: user_3@example.com

Operation started as user_2 to add user_3 to CogneeLab tenant/organization

Operation started by user_2, as tenant owner, to add user_3 to Researcher role inside the tenant/organization

Operation as user_3 to select CogneeLab tenant/organization as active tenant

...

Operation started as user_2, with CogneeLab as its active tenant, to give read permission to Researcher role for the dataset QUANTUM owned by user_2
User 2 could not give permission to the role as the QUANTUM dataset is not part of the CogneeLab tenant
```

**The grant lands once the dataset lives in the tenant, and `user_3` reads it.** No grant anywhere in the run names `user_3`: the final read succeeds purely through membership of `Researcher`, which is the reason to grant at the role level at all.

```text theme={null}
Operation started as user_2, with CogneeLab as its active tenant, to give read permission to Researcher role for the dataset QUANTUM owned by the CogneeLab tenant

Recall result as user_3 on the QUANTUM dataset owned by the CogneeLab organization:
...
2026-09-11T17:28:40.308421 [info     ] recall: 1 results across sources=['graph'] (session=-) [recall]
kind='graph_completion' search_type='GRAPH_COMPLETION' ...
```

## Before You Start

* Complete [Quickstart](/getting-started/quickstart) to understand basic operations
* Ensure you have [LLM Providers](/setup-configuration/llm-providers) configured — the ingestion calls and every `GRAPH_COMPLETION` recall in the script make live LLM calls
* Leave multi-user mode on: the whole demo depends on `ENABLE_BACKEND_ACCESS_CONTROL`, which is enabled by default, and on graph and vector backends that support per-dataset isolation — see [Permissions Setup](/setup-configuration/permissions) and [Multi-User Mode Overview](/core-concepts/multi-user-mode/multi-user-mode-overview)
* Run it from a checkout of the cognee repo: the script reads `artificial_intelligence.pdf` from the sibling `data/` folder next to it
* Point it at a scratch instance: it opens with `prune_data()` and `prune_system(metadata=True)`, and the second of those drops the relational database — users, tenants, and ACLs included — rather than deleting one dataset

## How It Works

### Stage 1: Create the User-Management Tables

```python theme={null}
    # Set up the necessary databases and tables for user management.
    await setup()
```

After the two prunes have emptied the instance, `setup()` recreates the relational tables the permission system lives in: principals, tenants, roles, and the ACL rows that join them to datasets. Nothing later in the script — not even creating the first user — works without it.

### Stage 2: Give Each Owner Their Own Dataset

```python theme={null}
    print("Creating user_1: user_1@example.com")
    user_1 = await create_user("user_1@example.com", "example")
    ai_remember_result = await cognee.remember(
        [explanation_file_path],
        dataset_name="AI",
        user=user_1,
        self_improvement=False,
    )
```

Passing `user=user_1` is what makes this ingestion belong to someone: the `AI` dataset is created under `user_1`, who is initially the only principal with any permission on it. `user_2` is created the same way a few lines down and remembers an inline passage about quantum computing into a `QUANTUM` dataset, which gives the rest of the script two datasets with two different owners to negotiate over.

### Stage 3: Address Datasets by ID, Not Name

```python theme={null}
    def get_dataset_id(remember_result):
        """Extract dataset_id from remember output."""
        return UUID(remember_result.dataset_id)

    # Get dataset IDs from remember results
    # Note: When we want to work with datasets from other users (recall, remember, and etc.) we must supply dataset
    # information through dataset_ids; using dataset names only looks for datasets owned by current user
    ai_dataset_id = get_dataset_id(ai_remember_result)
    quantum_dataset_id = get_dataset_id(quantum_remember_result)
```

Each `remember()` result carries the id of the dataset it wrote to, and those ids are how the script refers to datasets from here on. The comment names the reason: a dataset *name* is only ever resolved among the datasets the calling user owns, so `dataset_name="QUANTUM"` as `user_1` would look for a second, different dataset instead of reaching `user_2`'s. Ids are used for the owner's own data too: the first recall passes `dataset_ids=[ai_dataset_id]` and returns an answer immediately.

### Stage 4: Watch the Default Denial

```python theme={null}
    # But user_1 cant read the dataset owned by user_2 (QUANTUM dataset)
    print("\nRecall result as user_1 on the dataset owned by user_2:")
    try:
        await cognee.recall(
            query_type=SearchType.GRAPH_COMPLETION,
            query_text="What is in the document?",
            user=user_1,
            dataset_ids=[quantum_dataset_id],
        )
    except PermissionDeniedError:
        print(f"User: {user_1} does not have permission to read from dataset: QUANTUM")
```

The same recall that just worked against `user_1`'s own dataset raises `PermissionDeniedError` when it points at `user_2`'s, because ownership grants nothing to anyone else. Writes are refused on the same grounds: the block that follows sends a `remember()` at `dataset_id=quantum_dataset_id` as `user_1` and catches the identical error, since `user_1` holds neither read nor write on that dataset.

### Stage 5: Grant One User Read Access

```python theme={null}
    # We've shown that user_1 can't interact with the dataset from user_2
    # Now have user_2 give proper permission to user_1 to read QUANTUM dataset
    # Note: supported permission types are "read", "write", "delete" and "share"
    print(
        "\nOperation started as user_2 to give read permission to user_1 for the dataset owned by user_2"
    )
    await authorized_give_permission_on_datasets(
        user_1.id,
        [quantum_dataset_id],
        "read",
        user_2.id,
    )

    # Now user_1 can read from quantum dataset after proper permissions have been assigned by the QUANTUM dataset owner.
    print("\nRecall result as user_1 on the dataset owned by user_2:")
    recall_results = await cognee.recall(
        query_type=SearchType.GRAPH_COMPLETION,
        query_text="What is in the document?",
        user=user_1,
        dataset_ids=[quantum_dataset_id],
    )
    for result in recall_results:
        print(f"{result}\n")
```

The grant reads as who gets it, on what, which permission, and who is authorizing — the last argument is the acting user, and the call is refused unless that user is allowed to share the dataset. One `"read"` row later, the recall from Stage 4 is replayed unchanged and returns an answer. Only reads are open: remembering into `QUANTUM` as `user_1` would still fail, because `"write"` was never granted.

### Stage 6: Set Up a Tenant and a Role

```python theme={null}
    # Users can also be added to Roles and Tenants and then permission can be assigned on a Role/Tenant level as well
    # To create a Role a user first must be an owner of a Tenant
    print("User 2 is creating CogneeLab tenant/organization")
    tenant_id = await create_tenant("CogneeLab", user_2.id)

    print("User 2 is selecting CogneeLab tenant/organization as active tenant")
    await select_tenant(user_id=user_2.id, tenant_id=tenant_id)

    print("\nUser 2 is creating Researcher role")
    role_id = await create_role(role_name="Researcher", owner_id=user_2.id)

    print("\nCreating user_3: user_3@example.com")
    user_3 = await create_user("user_3@example.com", "example")

    # To add a user to a role he must be part of the same tenant/organization
    print("\nOperation started as user_2 to add user_3 to CogneeLab tenant/organization")
    await add_user_to_tenant(user_id=user_3.id, tenant_id=tenant_id, owner_id=user_2.id)

    print(
        "\nOperation started by user_2, as tenant owner, to add user_3 to Researcher role inside the tenant/organization"
    )
    await add_user_to_role(user_id=user_3.id, role_id=role_id, owner_id=user_2.id)
```

Granting per user does not scale past a handful of colleagues, so `user_2` builds an organization instead: a `CogneeLab` tenant, selected as the active tenant, and a `Researcher` role inside it. The order is forced by the model — a role can only be created by a tenant owner, and a user can only join a role once they are in the same tenant, which is why `user_3` is added to `CogneeLab` before being added to `Researcher`. `user_3` then selects `CogneeLab` as its own active tenant.

### Stage 7: Hit the Tenant-Scoping Constraint

```python theme={null}
    # Even though the dataset owner is user_2, the dataset doesn't belong to the tenant/organization CogneeLab.
    # So we can't assign permissions to it when we're acting in the CogneeLab tenant.
    try:
        await authorized_give_permission_on_datasets(
            role_id,
            [quantum_dataset_id],
            "read",
            user_2.id,
        )
    except PermissionDeniedError:
        print(
            "User 2 could not give permission to the role as the QUANTUM dataset is not part of the CogneeLab tenant"
        )
```

Handing the role the *original* `QUANTUM` dataset fails, and this refusal is the one worth understanding: `user_2` owns that dataset and owns the tenant, but the dataset was created before `CogneeLab` existed and therefore belongs to `user_2` personally. A grant is evaluated inside the acting user's active tenant, and a personal dataset is out of that scope — so the answer is the same `PermissionDeniedError` as Stage 4, for a completely different reason.

### Stage 8: Own the Dataset in the Tenant, Then Grant to the Role

```python theme={null}
    # Note: We need to update user_2 from the database to refresh its tenant context changes
    user_2 = await get_user(user_2.id)
    quantum_cognee_lab_remember_result = await cognee.remember(
        [text],
        dataset_name="QUANTUM_COGNEE_LAB",
        user=user_2,
        self_improvement=False,
    )

    # The recreated Quantum dataset will now have a different dataset_id as it's a new dataset in a different organization
    quantum_cognee_lab_dataset_id = get_dataset_id(quantum_cognee_lab_remember_result)
    print(
        "\nOperation started as user_2, with CogneeLab as its active tenant, to give read permission to Researcher role for the dataset QUANTUM owned by the CogneeLab tenant"
    )
    await authorized_give_permission_on_datasets(
        role_id,
        [quantum_cognee_lab_dataset_id],
        "read",
        user_2.id,
    )
```

The fix is to put the data where the role can be given access to it: `user_2` is re-read from the database so the in-memory object carries the new active tenant, and the same passage is remembered again as `QUANTUM_COGNEE_LAB` — a distinct dataset with its own id, this time owned inside `CogneeLab`. The personal `QUANTUM` dataset is untouched and still reachable by selecting `user_2`'s personal tenant. Granting `"read"` to `role_id` now succeeds, and the closing recall proves the payoff: `user_3`, who was never named in any grant, reads the tenant dataset as a member of `Researcher`.

## Run It

```bash theme={null}
uv run python examples/demos/permissions/user_permissions_and_access_control_example.py
```

The run is a single pass with no arguments and no interactive steps; [What to Expect](#what-to-expect) above walks through its output.

<Columns cols={2}>
  <Card title="Permission Snippets" icon="code" href="/guides/permission-snippets">
    Every call from this walkthrough as a standalone snippet, plus the permission names.
  </Card>

  <Card title="Permissions System Overview" icon="shield" href="/core-concepts/multi-user-mode/permissions-system/overview">
    How principals, datasets, and ACLs fit together behind these calls.
  </Card>

  <Card title="Multi-User Mode Overview" icon="users" href="/core-concepts/multi-user-mode/multi-user-mode-overview">
    What access control changes about storage and isolation across a deployment.
  </Card>

  <Card title="Permissions Setup" icon="shield" href="/setup-configuration/permissions">
    The environment variables and backend support the demo assumes.
  </Card>
</Columns>
