> ## Documentation Index
> Fetch the complete documentation index at: https://proxy-hopper.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Config Store

> The ConfigStore interface, its implementations, and how to add a new dialect.

See [Config Store](/concepts/config-store) for the concept and [Config Store (Admin)](/admin/deployment/config-store) for deploying one. This page is for contributors working on Proxy Hopper's own codebase — implementing a new `ConfigStore`, or touching `ProxyRepository`'s storage layer.

## The interface

`python_modules/proxy-hopper/src/proxy_hopper/config_store/base.py`, sibling to `backend/base.py`:

```python theme={}
@dataclass
class ConfigEntity:
    name: str
    data: dict
    static: bool
    mutable: bool
    updated_at: datetime   # naive UTC — see note below

class ConfigStore(ABC):
    async def start(self) -> None: ...
    async def stop(self) -> None: ...
    async def get(self, entity_type: str, name: str) -> ConfigEntity | None: ...
    async def set(self, entity_type: str, name: str, data: dict, *, static: bool, mutable: bool) -> None: ...
    async def delete(self, entity_type: str, name: str) -> None: ...
    async def list(self, entity_type: str) -> list[ConfigEntity]: ...
```

`entity_type` is `"target"`, `"provider"`, or `"pool"` — the three kinds `ProxyRepository` handles. `static`/`mutable` mirror the same flags already carried on `TargetConfig`/`ProxyProvider`/`IpPool`; storing them as first-class `ConfigEntity` fields (not just inside `data`) lets `ProxyRepository`'s guard checks (`if existing.static: raise ...`) read them without deserializing the full stored object.

<Note>
  `updated_at` is naive UTC in every implementation, by convention — not timezone-aware. SQLite's `DATETIME` column type doesn't reliably round-trip tz-aware Python datetimes, so every implementation normalizes to naive UTC rather than having that inconsistency surface only on one dialect.
</Note>

## Implementations

**`MemoryConfigStore`** (`config_store/memory.py`, core package) — a dict-of-dicts, keyed by `(entity_type, name)`. Test-only: `SqlConfigStore` with SQLite strictly dominates it for real deployments (same zero-external-dependency property, but actually durable across restarts), so this is never exposed as a CLI/chart option — it's what `configStoreUrl` defaults to when unset. Deep-copies `data` on both `set()` and `get()`/`list()`, so callers can't mutate the store's internal state through a returned `ConfigEntity`, or corrupt it by later mutating a dict they'd previously passed to `set()` — a real store gets this isolation for free from its JSON serialization round-trip; this in-memory double has to do it explicitly to behave the same way.

**`SqlConfigStore`** (`proxy_hopper_sql/config_store.py`, separate `proxy-hopper-sql` package) — SQLite or Postgres via a SQLAlchemy async engine. Dialect is disambiguated entirely by the connection URL's scheme (`sqlite+aiosqlite://` vs `postgresql+asyncpg://`) — no separate "store type" field, mirroring how `redis_url` alone already selects the Redis backend elsewhere in this codebase. The schema (`proxy_hopper_sql/schema.py`) is SQLAlchemy Core, not raw SQL strings and not the ORM — one `config_entities` table, composite primary key `(entity_type, name)`.

<Warning>
  `proxy-hopper-sql` is optional, like `proxy-hopper-redis` — the core `proxy-hopper` package must never import it at module scope. Every reference to it (in `cli.py`'s `migrate` command graft, and in `wiring.py`'s `build_repo()`) is a lazy import inside a function, guarded by `try`/`except ImportError`, exactly like the existing Redis backend wiring.
</Warning>

## Migrations

`proxy_hopper_sql/alembic/` holds the Alembic scaffold; `proxy_hopper_sql/migrations.py` wraps `alembic.command` programmatically (`upgrade`/`downgrade`/`heads`) — the `migrate` CLI command and the Helm chart's migration Job/initContainer both call this, never the `alembic` binary directly.

On Postgres, `upgrade()` wraps the actual migration in `pg_advisory_lock`/`pg_advisory_unlock`, so concurrent `migrate` invocations against the same database (e.g. two pods racing the same Helm migration Job) serialize instead of racing each other into a duplicate-table error on a fresh database. Not needed for SQLite — a single-writer local file already serializes.

## Adding a new dialect

SQLAlchemy already supports far more dialects than SQLite/Postgres — adding one (MySQL, for instance) is mostly a matter of:

1. Add the async driver to `proxy-hopper-sql`'s dependencies.
2. Confirm `SqlConfigStore`'s engine construction (`create_async_engine(url)`) needs no dialect-specific branching — it shouldn't, since the query layer is SQLAlchemy Core throughout.
3. Add an entry to the contract suite's `_STORE_FACTORIES` (see below) and, if the CI service container needs new infrastructure, extend `ci.yml`'s `helm-lint`/`test` jobs accordingly.
4. Extend the Helm chart's `configStore.dialect` enum and its migration-mechanism branch — Postgres-shaped dialects likely fit the existing `Job` pattern; anything requiring a local file follows the SQLite initContainer+PVC pattern instead.

## Testing

Every `ConfigStore` implementation is exercised against the same test bodies via a shared contract suite, parametrized over a `_STORE_FACTORIES` dict — the same pattern the `Backend` contract tests use for `memory`/`redis`:

* `python_modules/proxy-hopper/tests/test_config_store_contract.py` — the interface contract itself, `memory` only. Lives in the core package's own test suite since it has no cross-package dependency.
* `python_modules/tests/test_config_store_contract.py` — the same contract, plus `sqlite` and `postgres` (gated on a `POSTGRES_URL` environment variable, mirroring the existing `REDIS_URL`/fakeredis pattern). Lives in this cross-package project, not in `proxy-hopper/tests/`, specifically so that exercising `SqlConfigStore` doesn't make the *core* package's own test suite depend on `proxy-hopper-sql`.
* `python_modules/tests/test_repository_config_store_contract.py` — `ProxyRepository` CRUD and the provider→pool→target cascade, proven across the same `memory`/`sqlite`/`postgres` set, for the same reason.
* `python_modules/proxy-hopper-sql/tests/` — package-specific tests: schema creation, upgrade/downgrade/upgrade round-trips, the CI multi-head guard, and (Postgres-gated) the `pg_advisory_lock` concurrency test.

<Tip>
  Adding `sqlite` or `postgres` support to some new piece of test coverage almost never means writing a new test file — it means adding one factory entry to whichever `_STORE_FACTORIES` dict already fits, and every existing test in that file runs against the new implementation automatically.
</Tip>
