Every adapter in libs/ needs a provider API key, and all of them resolve keys the same
way. This page covers the resolution order, the two ways to supply credentials, and how
keys reach functions running on Modal.
Two ways to provide keys
# No Infisical required: pass the provider key directly on the command.
# The flag rides the call to the deployed Modal function and overrides
# the environment inside the container for that one invocation.
uv run gtm exa search "developer-first CRM tools" --exa-api-key <YOUR_EXA_API_KEY>
# One-time: copy the template and fill in your Infisical credentials.
cp .env.example .env.local
# Once per shell: load the bootstrap credentials.
set -a && source .env.local && set +a
# Keys are fetched from Infisical at function entry — no per-service flags.
infisical run --projectId "$INFISICAL_PROJECT_ID" --token "$INFISICAL_TOKEN" --env=prod -- \
uv run gtm exa search "developer-first CRM tools"
- API-key flags work without any vault. Commands that call a provider accept an
override flag (
--exa-api-key, --attio-api-key, --apollo-api-key, …) that is
forwarded to the deployed Modal function for that single invocation.
- Infisical is the zero-flags path: the CLI ships your Infisical bootstrap
credentials to the Modal function, which fetches the keys it needs at entry.
Key resolution inside libs/
Each adapter’s client resolves its key in a fixed order, documented in the client module
(for example libs/exa/client.py):
- An explicit
api_key= argument — used by tests and one-off scripts.
- The contextvar bound by
api_key_scope(...) — opened by webhook handlers and Modal
functions after fetching the key from Infisical.
- The adapter’s environment variable (for example
EXA_API_KEY) — the fallback for
plain-environment setups.
from libs.exa.client import api_key_scope
from libs.exa.search import search
with api_key_scope("<YOUR_EXA_API_KEY>"):
response = search(payload)
The contextvar scope exists for concurrency: multiple Modal inputs can run in the same
container, and a contextvar keeps keys from leaking between requests. Prefer
api_key_scope over mutating os.environ in long-lived processes.
The .env.local bootstrap (Infisical)
.env.local holds only the credentials that get you into the vault — every provider
secret lives in Infisical itself, never in the file:
cp .env.example .env.local # then fill in the values
set -a && source .env.local && set +a
| Variable | Purpose |
|---|
INFISICAL_TOKEN | Service token or machine-identity access token for the project. |
INFISICAL_PROJECT_ID | The Infisical project the token points at. |
INFISICAL_ENV | Environment slug to read against. Required at fetch time — there is no default, by design: a silent dev default could route production traffic at dev credentials. Match your project’s slugs (for example dev, stg, prod). |
INFISICAL_HOST | Optional. Only for self-hosted Infisical; defaults to https://app.infisical.com. |
There is no .infisical.json in the repo, so the infisical CLI does not auto-detect
the project — always pass --projectId, --token, and --env explicitly, as shown in
every example on this site.
Keys at runtime on Modal
Keyed CLI commands do not call providers from your laptop — they invoke functions
deployed on Modal (see the CLI contract). Keys reach those
functions through a bootstrap pattern in src/secrets_bootstrap.py:
- Each function is declared with
@with_secrets("<X>_API_KEY") and
secrets=[bootstrap_secret()].
bootstrap_secret() captures your INFISICAL_* credentials from the deploy-time
shell and ships them to Modal as a server-side secret — they never appear in image
layers or build logs.
- At function entry, the wrapper fetches each declared key from Infisical and opens the
matching
api_key_scope for the duration of the call.
The set of keys the bootstrap can bind is KEY_SCOPES in src/secrets_bootstrap.py:
APOLLO_API_KEY, ATTIO_API_KEY, CALCOM_API_KEY, EXA_API_KEY, LINEAR_API_KEY,
PARALLEL_API_KEY, and SLACK_BOT_TOKEN.
Do not bind named Modal secrets with modal.Secret.from_name(...) — the bootstrap
pattern replaced them. Adding a new key means: wire an api_key_scope contextvar in
libs/<x>/client.py, add the "<X>_API_KEY" entry to KEY_SCOPES, and decorate the
function with @with_secrets("<X>_API_KEY").
Environment variable reference
Provider keys, one per adapter:
| Environment variable | Adapter |
|---|
APOLLO_API_KEY | libs/apollo |
ATTIO_API_KEY | libs/attio |
CALCOM_API_KEY | libs/caldotcom |
EXA_API_KEY | libs/exa |
FATHOM_API_KEY | libs/fathom |
GRANOLA_API_KEY | libs/granola (API source mode) |
HARVEST_API_KEY | libs/harvest |
HOOKDECK_API_KEY | gtm webhook sync (registry generation) |
LINEAR_API_KEY | libs/linear |
OCTOLENS_API_KEY | libs/octolens |
PARALLEL_API_KEY | libs/parallel |
RESEND_API_KEY | libs/resend |
SANITY_API_TOKEN | libs/sanity |
SLACK_BOT_TOKEN | libs/slack |
Telemetry has its own set of producer- and collector-side variables — see the
telemetry overview.