State storage — LMDB or Postgres
CocoIndex persists target states and memoization results so it can detect changes and apply minimal updates between runs. Pick LMDB (zero-config, local) or Postgres (shared, managed) by the form of `db_path`, tune via `LmdbSettings` / `PgSettings`.
CocoIndex persists target states and memoization results so it can detect what changed between runs and apply only the necessary updates. This page covers where that state lives and how to configure the backend.
Backend selection
The backend is selected by the form of db_path:
db_path value | Backend |
|---|---|
Filesystem path or pathlib.Path (e.g. ./cocoindex.db) | LMDB (embedded, default) |
postgres://… or postgresql://… URL | Postgres |
The simplest way to set db_path is the COCOINDEX_DB environment
variable — it accepts either form:
# LMDB — local file (default)
export COCOINDEX_DB=./cocoindex.db
# Postgres — any DSN tokio-postgres accepts
export COCOINDEX_DB=postgres://cocoindex:[email protected]:5432/coco
Programmatic equivalents (override the env var):
# In a lifespan function
@coco.lifespan
def coco_lifespan(builder: coco.EnvironmentBuilder) -> Iterator[None]:
builder.settings.db_path = "postgres://localhost/coco"
yield
# Or directly when constructing Settings
settings = coco.Settings(db_path="postgres://localhost/coco")
If neither db_path nor COCOINDEX_DB is set, CocoIndex raises an
error at Environment construction time.
Picking a backend
| LMDB (default) | Postgres | |
|---|---|---|
| Setup | Zero-config — a directory on disk | Provision a Postgres database, set the DSN |
| Process model | Embedded, single-process | Single-process today; multi-process planned |
| Storage location | Local filesystem | External RDBMS |
| Scaling ceiling | Bounded by map_size (virtual address space, default 4 GiB) | Bounded by the Postgres instance |
| Best for | Local development, single-machine pipelines, CI | Shared deployments, managed RDBMS environments, audit-friendly storage |
| Tuning struct | coco.LmdbSettings | coco.PgSettings |
For most local development and single-machine production deployments, LMDB is the right choice — it has no operational overhead and is fast. Switch to Postgres when you want the engine state to live in a managed external database (centralized backup, separate from the host filesystem, etc.).
LMDB
The default. State lives in a local LMDB directory.
Configuration
The simplest setup — just point COCOINDEX_DB (or
Settings.db_path) at any filesystem path:
export COCOINDEX_DB=./cocoindex.db
Programmatic:
@coco.lifespan
def coco_lifespan(builder: coco.EnvironmentBuilder) -> Iterator[None]:
builder.settings.db_path = pathlib.Path("./cocoindex.db")
yield
# or
settings = coco.Settings(db_path=pathlib.Path("./cocoindex.db"))
Setting db_path in the lifespan or Settings takes precedence over
COCOINDEX_DB.
Tuning — LmdbSettings
The LMDB backend has two tunable knobs, grouped under
coco.LmdbSettings and attached to Settings as db_settings. The
defaults work well for most use cases.
| Setting | Default | Env Variable | Description |
|---|---|---|---|
max_dbs | 1024 | COCOINDEX_LMDB_MAX_DBS | Maximum number of named LMDB databases. Must be ≥ 1. |
map_size | 4294967296 (4 GiB) | COCOINDEX_LMDB_MAP_SIZE | Maximum size of the LMDB memory map in bytes. Must be > 0. |
When to adjust:
- Increase
map_sizeif you encounter LMDB “map full” errors. On 64-bit systems,map_sizeis a virtual address space reservation — setting it larger than needed is safe and does not consume physical memory. - Increase
max_dbsif you have an unusually large number of apps sharing a single database directory.
Via environment variables:
export COCOINDEX_LMDB_MAP_SIZE=8589934592 # 8 GiB
export COCOINDEX_LMDB_MAX_DBS=2048
Or programmatically:
settings = coco.Settings(
db_path=pathlib.Path("./cocoindex.db"),
db_settings=coco.LmdbSettings(
map_size=8 * 1024 * 1024 * 1024, # 8 GiB
max_dbs=2048,
),
)
Settings.from_env() picks up COCOINDEX_LMDB_MAX_DBS /
COCOINDEX_LMDB_MAP_SIZE automatically.
For backward compatibility, Settings still accepts lmdb_max_dbs
and lmdb_map_size as keyword arguments, and exposes them as
attributes (e.g., settings.lmdb_map_size = ...). These read and
write the same underlying values as
settings.db_settings.max_dbs / settings.db_settings.map_size.
Passing both db_settings= and the legacy keywords in the same
Settings(...) call raises ValueError.
Postgres (Plus)
Use the Postgres backend when you want CocoIndex’s internal state in an external, managed database.
Configuration
Set db_path to a Postgres connection URL. Any DSN that
tokio-postgres accepts works —
including host, port, user, password, dbname, sslmode,
and query-string options:
# Via env var
export COCOINDEX_DB=postgres://cocoindex:[email protected]:5432/coco
# With TLS
export COCOINDEX_DB="postgres://cocoindex:[email protected]:5432/coco?sslmode=require"
# Unix-socket / passwordless local
export COCOINDEX_DB=postgresql:///coco
Programmatic:
@coco.lifespan
def coco_lifespan(builder: coco.EnvironmentBuilder) -> Iterator[None]:
builder.settings.db_path = "postgres://cocoindex@db/coco"
yield
# or
settings = coco.Settings(db_path="postgres://cocoindex@db/coco")
Either postgres:// or postgresql:// is recognized; everything else
is treated as a filesystem path (LMDB).
Required Postgres privileges
On first use, CocoIndex creates an environment-level metadata table
(cocoindex_meta) and per-app tables (app_<sanitized>_<id>__paths,
__target, __idseq). The connecting role needs CREATE on the
target schema (and USAGE if it’s not public):
-- Minimal setup
CREATE DATABASE coco;
CREATE ROLE cocoindex WITH LOGIN PASSWORD 'secret';
GRANT CONNECT ON DATABASE coco TO cocoindex;
\c coco
GRANT USAGE, CREATE ON SCHEMA public TO cocoindex;
CocoIndex handles table creation, migration (versioned via
tables_version on cocoindex_meta), and per-app cleanup on
App.drop_app(). The first call against a fresh database performs a
one-time bootstrap that’s race-safe under concurrent first-time
callers (advisory lock around CREATE TABLE IF NOT EXISTS).
Tuning — PgSettings
| Setting | Default | Description |
|---|---|---|
pool_max_size | 16 | Maximum number of connections held open by the connection pool. |
schema | None | Postgres schema (namespace) to hold CocoIndex’s tables. When set, every connection’s search_path is pinned to it and the schema is created on first use (CREATE SCHEMA IF NOT EXISTS). When unset, tables land in the connection’s default search_path (usually public). Must be a simple lowercase identifier. |
settings = coco.Settings(
db_path="postgres://cocoindex@db/coco",
db_settings=coco.PgSettings(pool_max_size=32, schema="cocoindex"),
)
schema can also be set via the COCOINDEX_PG_SCHEMA environment
variable (picked up by coco.Settings.from_env()):
export COCOINDEX_DB=postgres://cocoindex@db/coco
export COCOINDEX_PG_SCHEMA=cocoindex
Putting CocoIndex’s tables in a dedicated schema keeps them out of
public and makes them easy to inspect, back up, or drop as a unit. The
connecting role needs CREATE on the database (to create the schema on
first use) in addition to the privileges below.
Operational notes
- Schema layout. Per app:
app_<sanitized_name>_<hex_id>__paths(tracking info, memoization, child existence, tombstones),__target(reverse-tracking ownership index),__idseq(per-app ID sequencer). Plus a single environment-widecocoindex_meta({key TEXT PRIMARY KEY, value JSONB}) holdingapp:<name>registrations. - Cleanup.
App.drop_app()drops all three per-app tables and removes thecocoindex_metarow in oneSERIALIZABLEtransaction. - Concurrency. A single per-environment connection pool serves
reads and writes. Concurrent writes are coalesced into a single
BEGIN ISOLATION LEVEL SERIALIZABLEtransaction; bodies share the transaction with inter-body flushing so the engine sees its own prior writes within the batch. - Multi-process. A single process per database today. Multi-process support (multiple workers sharing a Postgres backend) is on the roadmap.
See also
- App — configuring
db_pathprogrammatically via a lifespan. - Multiple environments — running isolated state stores side by side.