If application behaviour depends on values in a third-party dashboard, those values belong in a machine-checked inventory.
The Missing Half of Configuration as Code
I can keep application configuration in git and still suffer configuration drift. The reason is simple: part of the system lives outside the repository.
Stripe owns products and price IDs. An OAuth provider owns redirect URIs, scopes, and application status. A model gateway owns model identifiers and availability. A social platform owns approved capabilities. A deployment provider owns domains, environment variables, and webhook endpoints.
My code references those objects, but git cannot prove they still exist or mean what the code assumes.
Configuration is not truly versioned when the repository stores references but the provider owns their meaning. I close that gap by reading the external configuration into a normalised registry and validating my code against it.
The Registry Is an Inventory, Not a Secret Store
A checked registry contains identities and safe metadata:
{
"provider": "stripe",
"environment": "production",
"generatedAt": "2026-08-13T09:00:00Z",
"plans": {
"parent-core-monthly": {
"productId": "prod_example",
"priceId": "price_example",
"currency": "gbp",
"unitAmount": 1900,
"interval": "month",
"active": true
}
}
}
It does not contain API keys, client secrets, signing secrets, access tokens, or credentials. Those remain in 1Password or the deployment platform. The registry says which public or non-secret provider objects the application expects and what properties matter.
This separation is important. “Put configuration in git” is dangerous advice if configuration includes credentials. I version the contract, not the authority needed to mutate the provider.
The Four-Step Loop
The pattern has four stages:
- Fetch provider state through its official CLI or API.
- Normalise unstable output into a deterministic schema.
- Validate application references and cross-environment expectations.
- Diff the registry and gate unexpected changes.
The fetch step is read-only. For Stripe, it lists products and prices. For an OAuth application, it may read registered redirect URIs and enabled scopes. For a model gateway, it reads the current model catalog and capability metadata.
The normaliser removes fields that create meaningless diffs, such as retrieval timestamps, provider ordering, request IDs, and dashboard presentation fields. Records are sorted by stable keys and projected into the smallest schema needed by the application.
The validator then asks concrete questions:
- Does every identifier referenced by code exist?
- Is the referenced object active?
- Do amount, currency, cadence, region, or capability match expectations?
- Are staging and production intentionally different?
- Are deprecated objects still referenced?
- Has the provider gained an object that should be classified?
Generate, Then Validate
I prefer the provider to generate the registry and the repository to validate it. Hand-editing the file would only create a second source of truth.
A typical workflow looks like this:
provider CLI/API
|
v
normalise-provider-config
|
v
generated registry JSON
|
+--> schema validation
+--> application reference validation
+--> staging/production parity policy
+--> git diff or CI/CD artifact
For local development, I can regenerate the file on demand. In CI/CD, a scheduled or pre-release job reads the provider and compares the result with the committed expectation. If live access is inappropriate for pull requests, I validate code against the last approved snapshot on every PR and run live drift detection on a trusted schedule.
Propagate One Truth Through Every Layer
The registry should not become one more file that each runtime interprets differently. I use it as the input to generated, language-specific contracts:
live provider -> checked registry
|
+-> generated TypeScript constants and types
+-> generated Python models and constants
+-> generated SQL fixture for pgTAP assertions
TypeScript and Python import generated artifacts rather than maintaining separate identifier lists. The pgTAP suite uses the generated SQL fixture to assert that database rows, functions, constraints, or billing mappings expose the same logical plans and provider identities.
CI then proves two directions of truth. An upstream freshness test compares the registry with the provider’s current read-only state. Downstream synchronisation tests regenerate the TypeScript, Python, and SQL artifacts and fail if git contains a difference. Runtime tests in each layer confirm that those generated values are actually used.
The registry propagates truth, while the tests prove that the truth is current and has reached every layer. A green TypeScript test alone is insufficient if Python or Postgres still carries yesterday’s identifier.
Drift Needs Classification
Not every difference should block a release. I classify registry changes so the gate can respond proportionately.
| Change | Default response |
|---|---|
| Referenced object missing or inactive | Block |
| Price, currency, scope, or endpoint changed | Block and review |
| New unreferenced object | Warn and classify |
| Display name or description changed | Record |
| Expected staging-only difference | Allow by policy |
| Secret value changed | Never enter registry |
This avoids turning the registry into a noisy mirror of the provider dashboard. I only track fields that affect runtime behaviour, billing, access, compliance, or release safety.
A Registry Must Prove Its Freshness
A committed snapshot creates false confidence if its age is unknown. I record its provider environment, generation time, schema version, and retrieval status. Sensitive release paths reject expired snapshots.
I distinguish “provider returned no objects” from “provider could not be queried.” A failed fetch must not replace the last good registry. Generation validates a candidate before replacing the approved snapshot.
A stale registry is documentation. A fresh registry is evidence.
Environment Parity Is a Policy, Not Equality
Staging and production should not always be identical. Staging may use test-mode prices, sandbox OAuth applications, limited scopes, or private webhook endpoints.
The useful invariant is semantic parity. Both environments should expose the same application concepts, even when their provider IDs differ.
{
"logicalPlan": "parent-core-monthly",
"staging": "price_test_example",
"production": "price_live_example"
}
The registry maps stable logical names to environment-specific identities. Application code consumes the logical name. Environment configuration selects the concrete provider object. The validation gate proves that both objects satisfy the same contract.
Do Not Let the Sync Job Mutate Production
There is a crucial boundary between detection and reconciliation. A read-only registry job can run automatically. A job that creates, archives, reprices, or changes provider configuration can move money or break authentication.
I let automation report drift and generate a proposed change. I require explicit approval before destructive or financially meaningful provider mutations. The registry makes the difference legible before anyone acts.
Why This Helps Agents
Without a registry, an agent finds provider IDs scattered through code, templates, documentation, and screenshots. With one generated inventory, it can trace each logical identity across runtimes and run the synchronisation checks instead of guessing which copy is current.
The Rule
Whenever application behaviour depends on a third-party object, I ask whether the repository can enumerate and validate that object without exposing a secret.
If the answer is yes, I generate a checked registry. External configuration stops being dashboard folklore and becomes part of the release contract.

