> ## 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.

# Rust SDK Datasets and Access Configuration

> cognee-rust system paths, default user and dataset, per-dataset storage, and how access control and tenants scope search and delete.

Every value here resolves as `defaults < config.json < .env < env < code`
([precedence](https://docs.cognee.ai/rust/configuration.md#precedence)), and can be set from code with
the [config API](https://docs.cognee.ai/rust/configuration.md#runtime-configuration-api) or
[`cognee-cli config`](https://docs.cognee.ai/rust/configuration#cli-config-subcommand).

## System paths, users & datasets

| Env var | `Settings` field | Default |
| - | - | - |
| `COGNEE_SYSTEM_ROOT_DIRECTORY` | `system_root_directory` | `./.cognee_system` |
| `COGNEE_DATA_ROOT_DIRECTORY` | `data_root_directory` | `./.data_storage` |
| `CACHE_ROOT_DIRECTORY` | `cache_root_directory` | `./.cognee_cache` (the default ONNX model and tokenizer live, and are downloaded, under `<cache_root_directory>/models`; they previously defaulted to `./target/models`) |
| `COGNEE_DEFAULT_USER_ID` | `default_user_id` | nil UUID |
| `COGNEE_DEFAULT_DATASET_NAME` | `default_dataset_name` | `main_dataset` (the dataset `cognee-cli add`/`add-and-cognify`/`remember`/`improve` and the Python compat `add`/`cognify`/`add_and_cognify` use without an explicit one; a blank value falls back to `main_dataset`) |
| `DEFAULT_USER_EMAIL` / `DEFAULT_USER_PASSWORD` | `default_user_email` / `default_user_password` | `default_user@example.com` / *(empty)* (the password has no effect in this repository: the OSS build has no user accounts or login to give it to; Python uses it to make the default user loginable on its API server) |
| `ENABLE_BACKEND_ACCESS_CONTROL` | `enable_access_control` | `false` ([per-dataset storage](#per-dataset-storage); Python defaults to `true`) |

Setting `system_root_directory` cascades to the default `graph_file_path` and
`vector_db_url` unless those are set explicitly.

From the environment, every truthy spelling of `ENABLE_BACKEND_ACCESS_CONTROL`
(`true`, `1`, `yes`, `on`, any case) counts as `true`; `set`, a settings object
and a config file take a JSON boolean only.

### Per-dataset storage

**`ENABLE_BACKEND_ACCESS_CONTROL` / `enable_access_control` gives every dataset
its own graph and vector database**, as Python does
([design](https://github.com/topoteretes/cognee-rs/blob/main/docs/design/per-dataset-storage.md)).
Off (the default), every dataset shares one graph store and one vector store.
On:

* A dataset's databases are provisioned on its first use by the selected
  dataset database handlers (`GRAPH_DATASET_DATABASE_HANDLER`,
  `VECTOR_DATASET_DATABASE_HANDLER`) and recorded in the `dataset_database`
  table — Python's table, so the relational database stays shared between the
  SDKs. Storage is namespaced by the dataset's **owner**: the embedded handlers
  keep `{system_root_directory}/databases/{owner_id}/{dataset_id}.lbug` and
  `…/{dataset_id}.lance.db`.
* The graph and vector engines the components hand out route every call to
  the dataset of the current `cognee_models::dataset_scope`. A data call
  outside every dataset scope fails rather than reaching another dataset's
  data (`close` and `flush` act on every open engine). So `cognee-cli export`
  and `edge-reindex`, which run outside any dataset, fail in this mode.
* Deleting a dataset drops its databases; prune drops every dataset's, graph
  and vector together (a prune of one without the other is refused). The CLI
  and bindings wire this in; from the Rust SDK, build the `DeleteService` with
  `with_dataset_storage` and prune with `prune_system_with_storage`, both fed
  from `ComponentManager::dataset_storage`.
* The recorded embedding width is checked when a dataset's engines are
  opened: switching to a model of another width fails with an error naming
  the dataset, the stored and the configured model.
* It is not authorization. Permission checks (an attached `AclDb`, or the
  ownership fallback) run in both modes.
* **Nothing is migrated.** Turning it on starts every dataset empty; re-add and
  re-cognify. Turning it off again goes back to the shared stores.

> [!WARNING]
>
> **Migrating from Python:** Python's `.env.template` ships an uncommented
> `ENABLE_BACKEND_ACCESS_CONTROL=True`, and Python turns it on by default. A
> `.env` copied from it (cognee-rust loads `.env` automatically) turns on
> per-dataset storage here too. This used to be a configuration error
> (`ConfigError::Unsupported`); it is now accepted.

A configured provider without a handler is an error at the first graph or
vector component build (or the first `ComponentManager::dataset_storage` call)
naming the handler, the provider and the supported handlers (Python's
`multi_user_support_possible`). An adapter crate registers its own with
`ComponentRegistry::register_dataset_database_handler`. The built-in handlers:

| handler | kind | providers | storage per dataset | Postgres privilege |
| - | - | - | - | - |
| `ladybug` / `kuzu` | graph | `ladybug`, `kuzu` | `{system_root}/databases/{owner_id}/{dataset_id}.lbug` (`.pkl` when the provider is `kuzu`) | — |
| `lancedb` | vector | `lancedb` | `{system_root}/databases/{owner_id}/{dataset_id}.lance.db` | — |
| `memory` | both | graph `mock`; vector `mock`, `brute-force` | a fresh in-memory engine | — |
| `pgvector` (default for `pgvector`) | vector | `pgvector` | database `"{dataset_id}"` on the vector server, with the `vector` extension | `CREATEDB`, and the right to `CREATE EXTENSION vector` in a new database; Postgres 13+ |
| `pgvector_shared` | vector | `pgvector` | schema `ds_{dataset_id hex}` in the **relational** database, `search_path = ds_…, public` | `CREATE` on the relational database (and `vector` installed there, or the right to install it) |
| `postgres_graph` (default for `postgres`) | graph | `postgres`, `postgresql` | database `"{dataset_id}"` on the graph server | `CREATEDB`; Postgres 13+ |
| `postgres_graph_shared` | graph | `postgres`, `postgresql` | schema `ds_{dataset_id hex}` in the **relational** database, `search_path = ds_…` | `CREATE` on the relational database |

The Postgres handlers are compiled with the `pgvector` / `pggraph` features.
They store host, port, database and schema in `dataset_database`, never a
credential: every bind takes the username and password from the live
configuration — the resolved vector or graph URL for `pgvector` /
`postgres_graph`, the relational URL for the `_shared` handlers, which
therefore need a Postgres relational database. `CREATE DATABASE` runs on the
server's `postgres` maintenance database. Creation is idempotent and
serialized on the advisory locks Python takes, so concurrent first uses (in
any process) converge on one database. Deleting a dataset drops its database
(`DROP DATABASE … WITH (FORCE)`, which terminates sessions other processes
still hold on it, as Python does) or its schema (`DROP SCHEMA … CASCADE`).
Drops take the same advisory locks as creation, so a delete never interleaves
with another process's first bind of the same dataset.

> [!NOTE]
>
> The dedicated handlers (`pgvector`, `postgres_graph`) need **Postgres 13 or
> later** for `WITH (FORCE)`; there is no fallback for older servers.

`ConfigManager::set_application_access_control` and
`HandleState::with_application_access_control()` are no longer needed for the
flag to be accepted (it used to be refused without them); they are kept so
existing callers compile, and the value is only reported back by
`ConfigManager::application_access_control`.

What an attached `AclDb` changes for search, recall and delete:

* Explicit `dataset_ids` and dataset names are authorized against the caller's
  `read` grants (direct, tenant and role), now counting only datasets in the
  caller's tenant (`SearchRequest::tenant_id`, strict equality), as Python's
  `get_all_user_permission_datasets` does. This is a breaking change for a
  caller with no tenant: it used to get no tenant filter and now reads only
  tenant-less datasets.
* Over the shared store, an `AclDb` no longer scopes a search that names no
  dataset, nor filters the results of one that names datasets: it only
  authorizes the named ones.
* `DatasetManager::empty_dataset` and `delete_data` authorize by the `delete`
  grant on the dataset id, not ownership, as Python's `datasets.empty_dataset` /
  `delete_data` do, so a non-owner holding the grant can delete. `delete_data`
  refuses an item that exists but is not attached to that dataset, since the
  shared graph and vector cleanup would reach every dataset the item is in.
  Without an `AclDb`, ownership remains the authorization.
* Under an ACL (`AuthorizedDeleteService`), an owner-scoped delete scope whose
  `tenant_id` is not the caller's tenant is denied, and a data item without its
  dataset (`DeleteScope::Data { dataset_name: None }`) is refused with a
  validation error, as Python has no dataset-less item delete. A grant reaches
  another user's dataset only by id (`DatasetById` / `DataInDataset`): a
  by-name scope whose owner is not the caller is denied before any lookup.

How a search is scoped depends on storage, not on the ACL:

* **Per-dataset storage** (`ENABLE_BACKEND_ACCESS_CONTROL`): the retriever
  runs once per dataset, in that dataset's own databases —
  `SearchOrchestrator::search_datasets` returns one result per dataset
  (Python's `SearchResultPayload` list), `search` merges them (with several
  datasets, completions become one `Texts` answer per dataset; context, graphs
  and prompts are keyed by dataset id). Every search type works except
  `FEEDBACK`, which is not dataset data and runs once outside any dataset.
  Top-k is per dataset, and a dataset with nothing to search fails the search
  only when every dataset does. A request that names no dataset searches every
  dataset the caller may read, with or without an ACL, and returns an empty
  result when there is none (Python's ACL mode); every such search needs the
  caller's user id, and recall with an unknown caller gets no graph results.
  Without an ACL, "every dataset the caller may read" is the caller's own
  datasets in its tenant. A Rust SDK caller building its own `SearchBuilder`
  turns this on with `with_per_dataset_storage(true)` when
  `ComponentManager::dataset_storage` is `Some`; it is off by default, and the
  search then runs outside every dataset scope and fails.
* **Shared store** (the default): one unscoped search, whatever datasets the
  request names, after the permission check — Python's non-ACL mode. Results
  are no longer filtered by dataset membership, and the types that filter
  refused (`CYPHER`, `NATURAL_LANGUAGE`, `CHUNKS_LEXICAL`, `FEELING_LUCKY`,
  `CODING_RULES`, and community retrievers that do not declare
  `DatasetScoping::Membership`) run again under an `AclDb`.
  `SearchOrchestrator::refuses_dataset_scope` is always `false`. `CYPHER` and
  `NATURAL_LANGUAGE` stay gated by `ALLOW_CYPHER_QUERY`.

Deletes and dataset lookups are tenant-strict with or without an ACL:

* Every by-name dataset lookup (`update`, the delete services, search names,
  `forget`, `add`, the CLI) resolves among the owner's datasets in the caller's
  tenant, strictly: a `None` tenant finds only tenant-less datasets. The delete
  then runs by id. `get_dataset_by_name` is gone from `IngestDb` / `DeleteDb`;
  use `get_dataset_by_name_in_tenant`.
* `DeleteScope::{Data, Dataset, User}` carry a `tenant_id`, and look names up
  (or, for `Data { dataset_name: None }` and `User`, pick the owner's datasets)
  in that tenant only.
* `DeleteScope::User` and `forget(ForgetTarget::All)` delete the owner's
  datasets in the tenant one by one **by id** and stop at the first failure;
  the datasets before it stay deleted and are reported in
  `DeleteError::Partial`. Like Python's `datasets.delete_all`, they no longer
  delete the user's search history (the history table has no tenant); clear it
  with `DeleteDb::delete_search_history_for_user`. Unlike Python, `forget(All)`
  does not prune the whole session cache.
* A data item with no `Data` row inside an empty dataset deletes that dataset
  when `delete_dataset_if_empty` is set.
* `cognee-cli delete --all` deletes the caller's datasets in its tenant
  (`--tenant-id`), not the whole store; the library's plain `DeleteScope::All`
  is still the store-wide wipe.
* Without an ACL, `DatasetManager::list_data` / `has_data` require the dataset
  to be the caller's and in its tenant, and answer `NotFound` otherwise; the
  bindings' `hasData` / `listData` reject an unknown or foreign id, and the
  TS, Java and C `hasData` reject a non-UUID id instead of answering `false`
  (Python's `has` already did).

The per-dataset checks outside search — `DatasetManager` (`list_data`,
`has_data`, `empty_dataset`, `delete_data`), `AuthorizedDeleteService` and
`cognee::api::update` — use `AclDb::has_permission_in_tenant` with the caller's
tenant, strict equality. A grant on another tenant's dataset is denied with the
same error as no grant at all; under an ACL an unknown dataset id is denied too,
never "not found". `update` now reports a denial as
`ApiError::PermissionDenied` (previously `InvalidArgument`).
`DatasetManager::list_datasets` and `delete_all` count only grants on datasets in
the caller's tenant (`delete_all` uses the `delete` grants and deletes each
listed dataset by id), and `AuthorizedDeleteService`'s `DeleteScope::All` deletes
the caller's `delete`-authorized datasets in its tenant, like Python's
`datasets.delete_all`, rather than wiping every store.

Without an ACL, `DatasetManager::empty_dataset` / `delete_data` require the
dataset to be the caller's and in its tenant, then delete by id.

The ownership fallback that replaces the ACL — explicit
`dataset_ids`, dataset names and `DatasetManager::list_datasets` — also keeps
only datasets whose tenant equals the caller's: a tenant-less caller sees only
tenant-less datasets. Data written with a tenant (`cognee-cli add` / `remember` /
`improve --tenant-id`, or a binding's per-call `tenant` option) is therefore
found only by a read with the same tenant (`cognee-cli search` / `recall --tenant-id`, the bindings' `tenant` search option), and the tenant-less binding
dataset operations (`list_datasets`, `delete_all_datasets`, `empty_dataset`,
`delete_data`, and `forget` by dataset) no longer list or delete it.


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