Skip to main content
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):
  1. An explicit api_key= argument — used by tests and one-off scripts.
  2. The contextvar bound by api_key_scope(...) — opened by webhook handlers and Modal functions after fetching the key from Infisical.
  3. 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
VariablePurpose
INFISICAL_TOKENService token or machine-identity access token for the project.
INFISICAL_PROJECT_IDThe Infisical project the token points at.
INFISICAL_ENVEnvironment 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_HOSTOptional. 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:
  1. Each function is declared with @with_secrets("<X>_API_KEY") and secrets=[bootstrap_secret()].
  2. 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.
  3. 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 variableAdapter
APOLLO_API_KEYlibs/apollo
ATTIO_API_KEYlibs/attio
CALCOM_API_KEYlibs/caldotcom
EXA_API_KEYlibs/exa
FATHOM_API_KEYlibs/fathom
GRANOLA_API_KEYlibs/granola (API source mode)
HARVEST_API_KEYlibs/harvest
HOOKDECK_API_KEYgtm webhook sync (registry generation)
LINEAR_API_KEYlibs/linear
OCTOLENS_API_KEYlibs/octolens
PARALLEL_API_KEYlibs/parallel
RESEND_API_KEYlibs/resend
SANITY_API_TOKENlibs/sanity
SLACK_BOT_TOKENlibs/slack
Telemetry has its own set of producer- and collector-side variables — see the telemetry overview.
Last modified on July 14, 2026