The Registry

Call external APIs as data, not code

The Registry stores API providers, their endpoints, and stable service.verb aliases in the database. Your module calls an alias; the endpoint is a row, not a hardcoded URL. Change a provider without touching a line of application code.

Three layers, each validated against a closed vocabulary: providers/endpoints (auth is one of none · bearer · header · basic · query), shapes (rename/require on the way out, unwrap/error_path on the way back), and bindings (service.verb → provider+endpoint). An API that outgrows the vocabulary has crossed the boundary — it earns a dedicated client in your app instead.

1 · Store the credential as a secret

An API key is a secret — it never goes in a YAML or in code. Store it by name in the vault; the registry references the name, never the value.

fabbro vault set

Input is masked. fabbro vault list shows which providers have a credential; fabbro vault rm removes one. The value is Fernet-encrypted with your FERNET_KEY and resolved at call time.

2 · Register the provider

Three ways in, from most to least automatic. Discovery from an OpenAPI spec:

fabbro registry add petstore https://petstore.example.com --spec <spec-url> --secret-name PETSTORE_KEY

No spec URL? Drop --spec and the registry probes common spec locations from the base URL. When discovery can't reach anything, --manual is the always-works path: it registers the provider interactively, one endpoint at a time.

The declarative path — a YAML describing providers, endpoints, and bindings — is the one to commit to your repo so the registry is reproducible:

fabbro registry seed registry.yaml --dry-run

--dry-run prints the plan and writes nothing. Drop it (or add --yes to skip the prompt) to apply; --replace overwrites providers that already exist by name.

3 · Bind an endpoint as service.verb

A binding is the stable name your code calls. It points a service.verb alias at one provider's endpoint — the code names the service, never the provider, so swapping providers is a re-bind, not a code change.

fabbro registry bind pets.fetch petstore get_pet_by_id

fabbro registry unbind pets.fetch removes it. fabbro registry list is the human summary of everything registered.

4 · Call it from a module

From modules/<name>.py, reach the binding through service("..."). Path params and query values are passed as keyword arguments; the shape (rename, unwrap) is applied for you. You never build a URL or set an auth header by hand.

from core import registry_api as registry


def load_pet(pet_id: int) -> dict:
    # pets.fetch resolves to the bound provider+endpoint;
    # the stored secret is injected as the endpoint's auth requires.
    return registry.service("pets").fetch(id=pet_id)

An undeclared parameter fails locally with a Fix (the classic cityciyt typo) instead of being sent silently and returning a cryptic 400. Optional params an endpoint doesn't declare go through the explicit query={...} channel.

5 · Hand the registry to your AI agent

describe generates a usage guide of every registered service and verb — feed it to your coding agent so it calls the bindings correctly instead of inventing raw requests.

fabbro registry describe -o registry.md

fabbro registry export dumps the whole registry as seedable YAML — the round-trip back into seed on another environment.

Outbound rate limits aren't invented here. A call's limit belongs to the provider, which declares it in the response (429, Retry-After). Honouring that is on the roadmap; today call() retries network errors and hands the 429 back to you unmasked. This is separate from the inbound rate limiting that protects your own routes.
Why data, not code: endpoints live in the database, so adding, re-pointing, or retiring an API is a registry operation the doctor can check — not an edit scattered across modules. The closed vocabulary is the guardrail: it keeps generated integrations inside a shape the framework can validate.
← Back to playbooks