Skip to main content
Every value here resolves as defaults < config.json < .env < env < code (precedence), and can be set from code with the config API or cognee-cli config.

System paths, users & datasets

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). 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.
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: 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.
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.