# How to Manage and Configure Calliope System Settings: A Complete Guide

> Learn to manage and configure Calliope system settings effectively. Understand its two-layer configuration for static and dynamic settings to optimize your Calliope setup.

- Repository: [chrisimmel/calliope](https://github.com/chrisimmel/calliope)
- Tags: how-to-guide
- Published: 2026-02-27

---

**Calliope uses a two-layer configuration system where static service settings are managed via environment variables in [`calliope/settings.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/settings.py) and dynamic per-client configurations are stored in PostgreSQL using Piccolo ORM models, merged hierarchically at runtime by the `ConfigManager`.**

Calliope is an open-source generative media framework that supports hierarchical device management through a flexible configuration architecture. Understanding how to manage and configure Calliope system settings is essential for deploying the service across local development environments, cloud infrastructure, or distributed IoT device fleets. This guide examines the actual source code implementation to show you exactly how to modify static service behavior and customize dynamic client configurations.

## Understanding Calliope's Configuration Architecture

The codebase separates configuration into two distinct layers: **static service settings** that control global runtime behavior, and **dynamic per-client configurations** that determine how individual devices or device groups interact with the inference pipeline.

### Static Service Settings

Global system behavior is controlled by the `Settings` class in [`calliope/settings.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/settings.py), which extends Pydantic's `BaseSettings` to load values from environment variables at startup. These settings include database connections, cloud storage buckets, API keys, and network ports.

```python

# calliope/settings.py (lines 6-18)

class Settings(BaseSettings):
    APP_VERSION: str = "0.0.1"
    CLOUD_ENV: str = "local"
    CALLIOPE_API_KEY: str = "xyzzy"
    CALLIOPE_BUCKET_NAME: str = "artifacts.ardent-course-370411.appspot.com"
    MEDIA_FOLDER: str = "media"
    POSTGRESQL_HOSTNAME: str = "postgres"
    POSTGRESQL_USERNAME: str = "postgres"
    POSTGRESQL_PASSWORD: str = "postgres"
    POSTGRESQL_DATABASE: str = "postgres"
    PORT: str = "1234"

settings = Settings()

```

The `settings` object is a singleton that remains mutable at runtime. When you need to update a value programmatically, the `update()` method modifies both the object attribute and the underlying process environment.

```python

# calliope/settings.py (lines 24-30)

def update(self, name: str, value: str) -> None:
    """
    Updates settings and the system environment variable 'name' to 'value'.
    """
    setattr(self, name, value)
    os.environ[name] = value

```

### Dynamic Client Configuration

Device-specific behavior is stored in PostgreSQL and managed through Piccolo ORM models defined in [`calliope/tables/config.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/tables/config.py). This layer supports hierarchical inheritance: individual devices (sparrows) can inherit defaults from parent groups (flocks), which can themselves inherit from higher-level flocks.

The primary configuration tables are:

- **`SparrowConfig`** – Stores individual device or flock configurations with hierarchical parent references and JSONB parameter overrides.
- **`ClientTypeConfig`** – Defines reusable parameter templates that can be assigned to multiple clients via a `client_type` field.

```python

# calliope/tables/config.py (lines 18-38)

class SparrowConfig(Table):
    """The configuration of a sparrow, flock of sparrows, or flock of flocks."""
    client_id = Varchar(length=50, unique=True, index=True)
    description = Text(null=True, required=False)
    date_created = Timestamptz()
    date_updated = Timestamptz(auto_update=datetime.now)
    parent_flock_client_id = Varchar(length=50, null=True)
    follow_parent_story = Boolean()
    parameters = JSONB(null=True)  # Override parameters

    keys = JSONB(null=True)        # Override API keys

```

```python

# calliope/tables/config.py (lines 98-114)

class ClientTypeConfig(Table):
    """The definition of a client type."""
    client_id = Varchar(length=50, unique=True, index=True)
    description = Text(null=True, required=False)
    date_created = Timestamptz()
    date_updated = Timestamptz(auto_update=datetime.now)
    parameters = JSONB(null=True)

```

## Hierarchical Configuration Resolution

When a request arrives at the `/v1/frames/` endpoint, the **`ConfigManager`** in [`calliope/storage/config_manager.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/storage/config_manager.py) resolves the final configuration by walking up the flock hierarchy. Starting with the specific client ID, it retrieves each ancestor's `SparrowConfig`, merges the `parameters` and `keys` JSON objects, and applies any `ClientTypeConfig` overrides before adding strategy-specific defaults.

```python

# calliope/storage/config_manager.py (lines 61-99)

while sparrow_or_flock_id:
    sparrow_or_flock_config = await get_sparrow_config(sparrow_or_flock_id)
    # ... validation logic ...

    if sparrow_or_flock_config.parameters:
        sparrow_or_flock_params_dict = _get_non_default_parameters(
            load_json_if_necessary(sparrow_or_flock_config.parameters)
        )
    # Merge parent parameters into child parameters

    params_dict = {**sparrow_or_flock_params_dict, **params_dict}
    # Walk up to parent flock

    sparrow_or_flock_id = sparrow_or_flock_config.parent_flock_client_id

```

The result is a fully populated **`FramesRequestParamsModel`** containing resolved parameters, a **`KeysModel`** with merged API credentials, and a **`StrategyConfig`** defining the AI pipeline to execute.

## Updating Static Settings at Runtime

To modify global service configuration after startup, import the singleton `settings` object and call its `update()` method. This simultaneously changes the in-memory value and the environment variable, ensuring consistency for subprocesses or modules that read `os.environ`.

```python
from calliope.settings import settings

# Switch from local storage to GCP

settings.update("CLOUD_ENV", "gcp")
settings.update("CALLIOPE_BUCKET_NAME", "my-production-bucket")

print(f"Cloud environment is now {settings.CLOUD_ENV}")

```

## Configuring Clients Programmatically

### Creating Sparrows and Flocks

You can insert configuration records directly using the Piccolo ORM API. When creating a sparrow that belongs to a flock, set the `parent_flock_client_id` to establish the hierarchical relationship.

```python
from calliope.tables.config import SparrowConfig
from calliope.utils.file import FileMetadata
from datetime import datetime, timezone

async def create_sparrow():
    metadata = FileMetadata(
        date_created=datetime.now(timezone.utc),
        date_updated=datetime.now(timezone.utc),
        user_id="admin",
    )
    await SparrowConfig.from_pydantic(
        model=type("Tmp", (), {
            "id": "esp32_001",
            "description": "Living-room ESP32 board",
            "parent_flock_id": "living_room",
            "follow_parent_story": False,
            "parameters": {
                "output_image_width": 512, 
                "output_image_height": 512,
                "strategy": "fern"
            },
            "keys": {"OPENAI_API_KEY": "sk-your-key"},
        }),
        file_metadata=metadata,
    )

```

### Resolving Configuration for Debugging

To inspect exactly what configuration a specific client will receive during inference, use the `get_sparrow_story_parameters_and_keys()` function directly.

```python
from calliope.models import FramesRequestParamsModel
from calliope.storage.config_manager import get_sparrow_story_parameters_and_keys

async def debug_client_config(client_id: str):
    request = FramesRequestParamsModel(client_id=client_id)
    params, keys, strategy = await get_sparrow_story_parameters_and_keys(request)
    return {
        "resolved_params": params.dict(),
        "resolved_keys": keys.dict(),
        "strategy_slug": strategy.slug if strategy else None,
    }

```

### Client-Side Key Overrides

Clients can provide temporary API key overrides in the request payload. When the JSON body includes a `keys` object, the ConfigManager merges these with stored configuration, giving request-level values highest priority.

```json
{
  "client_id": "browser_12345",
  "client_type": "clio",
  "keys": { 
    "PINECONE_API_KEY": "pc-override-key",
    "OPENAI_API_KEY": "sk-temporary"
  },
  "input_text": "A quiet garden at dawn"
}

```

## Administrative Interface

For manual configuration management, Calliope exposes the **Piccolo Admin UI** at `http://localhost:8008/admin/` (or your hosted endpoint). This web interface provides CRUD operations for:

- `SparrowConfig` – Create individual devices or flock containers
- `ClientTypeConfig` – Define reusable client templates  
- `StrategyConfig` – Manage AI pipeline configurations

All changes made through the UI are immediately persisted to PostgreSQL and reflected in subsequent API requests.

## Summary

- **Static settings** in [`calliope/settings.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/settings.py) control global service behavior like database connections and cloud storage, loaded from environment variables and mutable at runtime via `settings.update()`.
- **Dynamic configurations** use Piccolo ORM models (`SparrowConfig`, `ClientTypeConfig`) stored in PostgreSQL to define per-device and per-flock behavior.
- The **`ConfigManager`** in [`calliope/storage/config_manager.py`](https://github.com/chrisimmel/calliope/blob/main/calliope/storage/config_manager.py) implements hierarchical resolution, walking from specific clients up through parent flocks to merge parameters and API keys.
- You can manage configurations programmatically via the ORM, through the built-in Piccolo Admin UI, or by providing override keys in individual API requests.

## Frequently Asked Questions

### How do I change the database connection string in Calliope?

Set the `POSTGRESQL_HOSTNAME`, `POSTGRESQL_USERNAME`, `POSTGRESQL_PASSWORD`, and `POSTGRESQL_DATABASE` environment variables before starting the service, or call `settings.update("POSTGRESQL_HOSTNAME", "new-host")` at runtime. The [`piccolo_conf.py`](https://github.com/chrisimmel/calliope/blob/main/piccolo_conf.py) file uses these settings to configure the Piccolo database engine.

### Can I override API keys for specific devices without changing environment variables?

Yes. Store device-specific keys in the `keys` JSONB column of the `SparrowConfig` table for that device. These overrides take precedence over global environment variables but can be superseded by keys provided in the actual API request payload.

### What is the difference between a Sparrow and a Flock in Calliope configuration?

Both use the same `SparrowConfig` table. A **Sparrow** represents an individual client device with a unique `client_id`. A **Flock** is also a `SparrowConfig` entry, but other entries reference it via `parent_flock_client_id`. Flocks allow you to set default parameters for groups of devices, which individual sparrows can override through their own `parameters` JSON.

### How does configuration inheritance work when a device belongs to multiple hierarchy levels?

The `ConfigManager` performs a bottom-up merge. It starts with the specific device's `parameters`, then climbs the hierarchy through each `parent_flock_client_id`, merging parent parameters on top of child parameters at each level. Finally, it applies `ClientTypeConfig` parameters and strategy defaults. This means higher-level (ancestor) values override lower-level values unless the child explicitly sets its own parameters.