> ## 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

> How Proxy Hopper persists provider/pool/target config created via the admin API, and why that's a separate concern from the Redis/memory backend.

## What is the config store?

Proxy Hopper's runtime state splits into two kinds, stored in two different places:

* **Operational state** — IP queues, rate-limit counters, quarantine timers, identity/cookie jars. Ephemeral by design: losing it on restart just means every IP starts back at "not yet used" — a cold cache, not a data-loss event. This lives in the **backend** (`memory` or `redis`).
* **Durable config** — providers, IP pools, and targets created or edited through the admin API. Losing this on restart is a real regression: an operator's changes silently disappear. This lives in the **config store** (`memory`, `sqlite`, or `postgres`).

<Note>
  Config defined in your YAML file is never at risk either way — it's reloaded from disk on every restart regardless of config store. The config store only matters for changes made *after* startup, through the admin API (GraphQL mutations, or the admin UI).
</Note>

```
Admin API mutation
       │
       ▼
ProxyRepository ──► ConfigStore (memory / sqlite / postgres)   durable config
       │
       └────────► Backend (memory / redis)                     publish "changed"
                          │
                          ▼
                   other instances re-read from ConfigStore
```

## Why this is a separate abstraction from the backend

Earlier versions of Proxy Hopper stored provider/pool/target config as JSON blobs in the same backend used for operational state — meaning "durable config" only survived a restart if you'd also opted into `backend: redis` with persistence enabled, purely as a side effect of a decision made for an unrelated reason (multi-replica pool sharing).

That coupling is gone. Config durability and operational-state sharing are now two independent choices:

|                              | Config store                                          | Backend                                                 |
| ---------------------------- | ----------------------------------------------------- | ------------------------------------------------------- |
| What it stores               | providers, pools, targets (admin-API-mutable)         | IP queues, counters, quarantine, identities             |
| Options                      | `memory` (default, not durable), `sqlite`, `postgres` | `memory`, `redis`                                       |
| Losing it on restart         | Operator's changes disappear — a real regression      | Cold start — expected, cheap to rebuild                 |
| Needed for multi-replica HA? | Only if replicas need to *share* config (see below)   | Yes, if replicas need to share IP pool/quarantine state |

A single-replica deployment with `backend: memory` can now durably persist admin-API config via `sqlite`, with zero Redis anywhere in the deployment. A multi-replica deployment can share config across an admin process and proxy runners via `postgres`, independent of whether pool state is shared via `redis` or kept private per replica via `memory`.

## Notify-then-reconcile

When a provider, pool, or target changes, Proxy Hopper publishes a change event over the backend's pub/sub channel — but the event itself carries no data, just which entity changed (`{entity, type, name}`). Every recipient reacts by re-reading the current value from the config store, not by trusting a payload embedded in the message.

This matters because pub/sub messages can be missed — a replica that's briefly disconnected when a change fires never sees that specific message again. Under the old design (the full serialized entity embedded in the event), a missed message meant permanently stale state until the next unrelated change happened to touch the same entity. Under this design, a missed message just means the next signal — or the periodic reconcile most consumers already run — catches the recipient back up, because the config store, not the pub/sub message, is the single source of truth.

## SQLite vs Postgres

<Tabs>
  <Tab title="SQLite">
    A local file — no external service to run. Good fit for single-replica deployments (the same class of deployment `backend: memory` already targets): dev environments, small single-node deployments, anywhere you want durable config without operating a database server.

    Single-writer by nature: one process, one file. See [Config Store (Admin)](/admin/deployment/config-store) for the exact topology constraints this implies under Kubernetes.
  </Tab>

  <Tab title="Postgres">
    A real multi-writer server. Required once more than one process needs to read or write config concurrently — a multi-replica proxy deployment, or an admin server running as a separate process/pod from the proxy runners, both pointed at the same durable config.
  </Tab>
</Tabs>

Both dialects go through the same `ConfigStore` interface and the same Alembic-managed schema — switching between them is a connection-string change, not a data migration you write yourself.

## Enabling it

Unset by default — admin-API config does not survive a restart until you opt in:

```yaml theme={}
server:
  configStoreUrl: "sqlite+aiosqlite:///./data/config.db"
  # or: "postgresql+asyncpg://user:pass@host/db"
```

Schema migrations are applied separately, via `proxy-hopper migrate` — see [Config Store (Admin)](/admin/deployment/config-store) for deployment details (including the Helm chart's automatic handling of this) and [Config Reference](/admin/configuration/reference#server) for the full field list.

<Note>
  Existing Redis-persisted config from before this feature existed is not migrated automatically — new deployments (and upgrades that opt into a config store for the first time) seed from YAML the same way they always have.
</Note>

## Going fully database-free

The config store makes durability *possible* — it doesn't make it required. If you don't want any database in your deployment at all, pair the default (unset `configStoreUrl`) with `server.adminReadOnly: true`: the admin server stays deployable for monitoring (status, targets, pools, providers, metrics all still query fine), but every mutation is rejected, so there's no ephemeral admin-API state to lose in the first place — all config comes from the YAML file, full stop. See [Admin Server — Read-only mode](/admin/admin-server/overview#read-only-mode).
