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 thedataset_databasetable — 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}.lbugand…/{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 (closeandflushact on every open engine). Socognee-cli exportandedge-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
DeleteServicewithwith_dataset_storageand prune withprune_system_with_storage, both fed fromComponentManager::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.
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_idsand dataset names are authorized against the caller’sreadgrants (direct, tenant and role), now counting only datasets in the caller’s tenant (SearchRequest::tenant_id, strict equality), as Python’sget_all_user_permission_datasetsdoes. 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
AclDbno 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_datasetanddelete_dataauthorize by thedeletegrant on the dataset id, not ownership, as Python’sdatasets.empty_dataset/delete_datado, so a non-owner holding the grant can delete.delete_datarefuses 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 anAclDb, ownership remains the authorization.- Under an ACL (
AuthorizedDeleteService), an owner-scoped delete scope whosetenant_idis 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.
- Per-dataset storage (
ENABLE_BACKEND_ACCESS_CONTROL): the retriever runs once per dataset, in that dataset’s own databases —SearchOrchestrator::search_datasetsreturns one result per dataset (Python’sSearchResultPayloadlist),searchmerges them (with several datasets, completions become oneTextsanswer per dataset; context, graphs and prompts are keyed by dataset id). Every search type works exceptFEEDBACK, 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 ownSearchBuilderturns this on withwith_per_dataset_storage(true)whenComponentManager::dataset_storageisSome; 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 declareDatasetScoping::Membership) run again under anAclDb.SearchOrchestrator::refuses_dataset_scopeis alwaysfalse.CYPHERandNATURAL_LANGUAGEstay gated byALLOW_CYPHER_QUERY.
- 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: aNonetenant finds only tenant-less datasets. The delete then runs by id.get_dataset_by_nameis gone fromIngestDb/DeleteDb; useget_dataset_by_name_in_tenant. DeleteScope::{Data, Dataset, User}carry atenant_id, and look names up (or, forData { dataset_name: None }andUser, pick the owner’s datasets) in that tenant only.DeleteScope::Userandforget(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 inDeleteError::Partial. Like Python’sdatasets.delete_all, they no longer delete the user’s search history (the history table has no tenant); clear it withDeleteDb::delete_search_history_for_user. Unlike Python,forget(All)does not prune the whole session cache.- A data item with no
Datarow inside an empty dataset deletes that dataset whendelete_dataset_if_emptyis set. cognee-cli delete --alldeletes the caller’s datasets in its tenant (--tenant-id), not the whole store; the library’s plainDeleteScope::Allis still the store-wide wipe.- Without an ACL,
DatasetManager::list_data/has_datarequire the dataset to be the caller’s and in its tenant, and answerNotFoundotherwise; the bindings’hasData/listDatareject an unknown or foreign id, and the TS, Java and ChasDatareject a non-UUID id instead of answeringfalse(Python’shasalready did).
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.