Database Setup Guide¶
This guide covers setting up PostgreSQL (with Apache AGE), ClickHouse, Apache Iggy and Valkey for use with imbi-common.
For local development, moon run root:services starts all of them from
compose.ci.yaml and writes their ports to .env.test.
PostgreSQL + Apache AGE Setup¶
imbi-common's graph layer runs on PostgreSQL with the Apache AGE extension for Cypher queries, plus pgvector for similarity search.
Using the Project Docker Image¶
The project ships a pre-built PostgreSQL image with all required extensions
installed (ghcr.io/aweber-imbi/postgres:latest). This is the easiest path
for local development:
# Via project compose setup (recommended)
moon run root:services
# Or run the image directly
docker run -d \
--name postgres-age \
-p 5432:5432 \
-e POSTGRES_PASSWORD=secret \
ghcr.io/aweber-imbi/postgres:latest
The image bundles: Apache AGE, pgvector, pg_cron, and pgtap. AGE is loaded
automatically via shared_preload_libraries in postgresql.conf.
Production Setup¶
For production deployments:
- Use a managed PostgreSQL service (RDS, Cloud SQL, etc.) with the AGE extension available, or self-host with the same custom image.
- Create a dedicated database user with limited privileges.
- Configure TLS for all connections.
- Set
max_pool_sizebased on your workload.
Example production configuration:
[postgres]
url = "postgresql://imbi_app:strong-password@db-prod:5432/imbi"
graph_name = "imbi"
max_pool_size = 20
Schema Initialization¶
No SQL init scripts are necessary. graph_lifespan (or
graph.initialize()) creates the AGE graph, vertex labels, indexes, the
embeddings table, and supporting functions on startup. The vertex labels
come from graph/schemata.toml in imbi-common.
from imbi.common import graph
# Initialize schema and open the pool (use graph_lifespan in FastAPI)
await graph.initialize()
ClickHouse Setup¶
Using Docker¶
Run ClickHouse locally with Docker:
docker run -d \
--name clickhouse \
-p 8123:8123 -p 9000:9000 \
-v $HOME/clickhouse/data:/var/lib/clickhouse \
clickhouse/clickhouse-server:latest
Production Setup¶
For production deployments:
- Use ClickHouse cluster for high availability
- Enable authentication and TLS
- Configure replication for data redundancy
- Set up proper TTLs for GDPR compliance
- Monitor disk usage and query performance
Example production configuration:
Schema Initialization¶
Initialize ClickHouse schemas from the bundled schemata.toml:
from imbi.common import clickhouse
# Create schemas from schemata.toml (called explicitly during setup)
await clickhouse.setup_schema()
setup_schema() runs only CREATE ... IF NOT EXISTS. Services do not
call it at startup. When a release adds a table, run
imbi-api setup-clickhouse (or the full imbi-api setup) to create it.
For a ClickHouse cluster, set CLICKHOUSE_CLUSTER_NAME. See
Clustered Deployments.
Apache Iggy Setup¶
Imbi services do not write rows to ClickHouse. They publish the rows to
Apache Iggy, and the Iggy connectors runtime (iggy-connect) runs the
ClickHouse sink that writes them. See Iggy and
ADR 0019.
You need two processes:
| Process | Image | Purpose |
|---|---|---|
iggy |
ghcr.io/aweber-imbi/iggy |
The Iggy server. Clients connect on TCP port 8090 |
iggy-connect |
The same image, with IGGY_MODE=connectors |
The connectors runtime that runs the ClickHouse sink |
Set IGGY_URL on every Imbi service, for example
iggy+tcp://iggy:iggy@iggy:8090. A service creates the streams and
topics at startup. The connectors runtime stops if a stream does not
exist, so start it after an Imbi service has started. compose.yaml
sets restart: on-failure on it for this reason.
The connectors runtime gets its sink configuration from imbi-api at
/iggy/connectors under the API prefix (for example
http://imbi:8000/api/iggy/connectors). Set the same key in
IGGY_CONNECTORS_API_KEY on the runtime and in
IMBI_IGGY_CONNECTORS_API_KEYS on imbi-api. Without the key,
imbi-api answers 503. The runtime reads the configuration one time,
at startup, so restart it after an upgrade that adds a stream or a
topic. For a working example, see the iggy and iggy-connect services
in compose.yaml.
In compose.ci.yaml, iggy-connect is in the connect profile, and
iggy-config-server serves the sink configuration in place of
imbi-api.
Valkey Setup¶
Imbi services connect to Valkey through valkey_lifespan. Set VALKEY_URL
(default valkey://localhost:6379/0). The setting accepts only the
valkey:// scheme. See Valkey Client.
compose.ci.yaml runs valkey/valkey:8-alpine. compose.yaml has no
valkey service.
Database Credentials¶
Environment Variables¶
The recommended way to provide credentials:
# PostgreSQL
export POSTGRES_URL="postgresql://imbi_app:password@localhost:5432/imbi"
# ClickHouse
export CLICKHOUSE_URL="clickhouse+http://localhost:8123"
Configuration File¶
For local development, use a config file:
# config.toml
[postgres]
url = "postgresql://postgres:secret@localhost:5432/imbi"
[clickhouse]
url = "clickhouse+http://localhost:8123"
Warning
Never commit config files with credentials to version control!
Secrets Management¶
For production, use a secrets manager:
- AWS: AWS Secrets Manager or Parameter Store
- GCP: Secret Manager
- Azure: Key Vault
- HashiCorp: Vault
- Kubernetes: Sealed Secrets or External Secrets Operator
Load secrets at runtime and set environment variables before initializing the database clients.
Connection Pooling¶
PostgreSQL¶
The graph client maintains a psycopg connection pool configured via settings:
- min_pool_size: Connections kept open at idle
- max_pool_size: Maximum concurrent connections
ClickHouse¶
The ClickHouse client is used for queries and schema setup only. The Iggy sink does all inserts, in batches.
Testing Connections¶
Verify database connectivity:
import asyncio
from imbi.common import graph, clickhouse, logging
async def test_connections() -> None:
logging.configure_logging(dev=True)
# Test PostgreSQL + AGE. cypher() fails until the graph exists.
await graph.initialize()
db = graph.Graph()
await db.open()
# execute() wraps the query in AGE cypher(), so use Cypher, not SQL
rows = await db.execute("RETURN 1", columns=["test"])
print(f"PostgreSQL connected! Test value: {rows[0]['test']}")
await db.close()
# Test ClickHouse
result = await clickhouse.query("SELECT 1 as test")
print(f"ClickHouse connected! Test value: {result[0]['test']}")
asyncio.run(test_connections())
Troubleshooting¶
PostgreSQL Connection Issues¶
Problem: OperationalError: could not connect to server
Solutions:
- Verify PostgreSQL is running: docker ps | grep postgres
- Check port 5432 is accessible
- Verify credentials in POSTGRES_URL
- Check PostgreSQL logs: docker logs postgres-age
AGE Extension Issues¶
Problem: ERROR: extension "age" does not exist
Solutions:
- Ensure you are using the custom ghcr.io/aweber-imbi/postgres:latest image
- Verify shared_preload_libraries includes age in postgresql.conf
ClickHouse Connection Issues¶
Problem: Connection timeout or refused
Solutions:
- Verify ClickHouse is running: docker ps | grep clickhouse
- Check port 8123 is accessible
- Verify the URL scheme (clickhouse+http:// not bare http://)
- Check ClickHouse logs: docker logs clickhouse
Schema Creation Failures¶
Problem: Constraint or index creation fails
Solutions: - Check for existing data that violates constraints - Verify database user has CREATE privileges - Review database logs for detailed error messages
Docker Compose¶
Do not copy a compose file into your project. Use the files in the repository root:
compose.ci.yamlholds the services for the tests: ClickHouse, Iggy, the connectors runtime, Mailpit, Ministack, PostgreSQL, Valkey and Jaeger.moon run root:servicesstarts it.compose.yamlruns the all-in-one Imbi image with its databases (docker compose up --build).