Skip to content

Authentication

The SDK authenticates using an Affinity API key.

from affinity import Affinity

with Affinity(api_key="your-api-key") as client:
    me = client.whoami()
    print(me.user.email)

Resolution chain

The SDK and CLI resolve the API key through the following chain (first non-empty value wins):

  1. The key you pass explicitly — Affinity(api_key="…") in the SDK; --api-key-file <path> or --api-key-stdin in the CLI (the CLI has no --api-key flag)
  2. AFFINITY_API_KEY — standard environment variable
  3. AFFINITY_API_KEY_FILE — path to a file containing the key, or a .env file / named pipe with AFFINITY_API_KEY= (12-factor / Docker secrets / 1Password)
  4. AFFINITY_API_KEY_COMMAND — shell command whose stdout is the key (credential-helper)
  5. User config file — api_key in config.toml, written by xaffinity config setup-key --scope user (file mode 0600 on Unix; xaffinity config path prints the location). CLI only.

The SDK uses steps 1–4; step 5 is CLI only. When the CLI uses a key flag while AFFINITY_API_KEY, AFFINITY_API_KEY_FILE or AFFINITY_API_KEY_COMMAND is also set, it warns that those are ignored (the file is not read and the command is not run). With load_dotenv=True (SDK) or --dotenv (CLI), a .env file is loaded into the environment before step 2, so its AFFINITY_API_KEY counts as step 2. That is how a key saved by xaffinity config setup-key --scope project (a .env in the current directory) is used: run commands with --dotenv.

Empty string is treated as unset at every step (safe against stale export AFFINITY_API_KEY= lines).

Environment variables

If you prefer reading from the environment:

from affinity import Affinity

# Reads AFFINITY_API_KEY by default
client = Affinity.from_env()

# Use a custom environment variable name
client = Affinity.from_env(env_var="MY_AFFINITY_KEY")

For local development, you can load a .env file (requires python-dotenv):

from affinity import Affinity

# Load .env from current directory
client = Affinity.from_env(load_dotenv=True)

# Load from a specific path
client = Affinity.from_env(load_dotenv=True, dotenv_path=".env.local")

AFFINITY_API_KEY_FILE — file-based secrets

Set this env var to the path of a file containing your API key. Used by Docker secrets, Kubernetes mounted Secrets, and Hashicorp Vault agent sidecars.

# Docker
docker run -e AFFINITY_API_KEY_FILE=/run/secrets/affinity_api_key …

# Kubernetes — mount a Secret as a file and set the env var
env:
  - name: AFFINITY_API_KEY_FILE
    value: /etc/secrets/affinity-api-key

The file can hold just the key, or be a .env file: the AFFINITY_API_KEY= line is used (export and quotes are allowed). The same applies to --api-key-file.

1Password Environments. An Environment's locally mounted .env file is a named pipe that 1Password fills when it is read, after you approve the read. Point the variable at it:

export AFFINITY_API_KEY_FILE=~/projects/crm/.env   # the path you chose when mounting it
xaffinity whoami

The first read asks you to approve it in 1Password; if 1Password is locked, it asks you to unlock it. The CLI waits up to 30 seconds for that. Further reads need nothing until 1Password locks again, and several commands can read at once. If 1Password isn't running or the read isn't approved, the CLI stops with a message saying so. The key is never written to disk. config check-key reports a named pipe as configured without reading it (so it never prompts).

While 1Password is unlocked, any program on your computer can read the mounted file (that is how 1Password's locally mounted .env files work), so a short auto-lock time in 1Password limits that window.

On Posix systems, a UserWarning is emitted if the file is group- or world-readable (mode 0644 or looser). Use chmod 600 to silence it. The check is a no-op on Windows. An empty file or missing path raises ValueError in the SDK (Affinity.from_env()); the CLI reports it as a usage error (exit code 2).

AFFINITY_API_KEY_COMMAND — command-based secrets

Set this env var to a shell command. The SDK runs it at startup and uses its stdout as the key. Follows the same convention as git credential.helper, gpg --passphrase-program, and similar tools.

# 1Password CLI
export AFFINITY_API_KEY_COMMAND="op read op://Personal/Affinity/credential"

# macOS Keychain
export AFFINITY_API_KEY_COMMAND="security find-generic-password -a affinity -w"

# pass (Unix password manager)
export AFFINITY_API_KEY_COMMAND="pass show affinity/api-key"

# HashiCorp Vault
export AFFINITY_API_KEY_COMMAND="vault kv get -field=api_key secret/affinity"

The default timeout is 30 seconds; override with AFFINITY_API_KEY_COMMAND_TIMEOUT=<seconds>. A non-zero exit code, empty stdout, or timeout raises ValueError in the SDK, or a usage error (exit code 2) in the CLI. stderr is included in the error message, capped at 500 chars.

For defensive "no writes" usage (scripts, audits), disable writes via policy:

from affinity import Affinity
from affinity.policies import Policies, WritePolicy

client = Affinity.from_env(policies=Policies(write=WritePolicy.DENY))

CLI Authentication

For the CLI, use the built-in setup commands:

# Check if a key is configured
xaffinity config check-key

# Set up a new key securely (hidden input)
xaffinity config setup-key

See CLI Authentication for details.

Caveats

.env files can silently shadow AFFINITY_API_KEY_FILE / _COMMAND

load_dotenv=True (SDK) and --dotenv (CLI) load .env files before the resolver reads any environment variable. By default, dotenv values do not override already-set shell variables (dotenv_override=False).

The resolver checks AFFINITY_API_KEY first, then AFFINITY_API_KEY_FILE, then AFFINITY_API_KEY_COMMAND. So if your .env file contains AFFINITY_API_KEY=… and you also have AFFINITY_API_KEY_FILE set in your shell:

  • After dotenv load, the shell-set AFFINITY_API_KEY_FILE is unchanged.
  • But AFFINITY_API_KEY is now also set (from .env).
  • The resolver returns the .env value at step 2; the file at the _FILE path is never read.

This is rarely intended. If you mix dotenv and _FILE/_COMMAND, either:

  • Keep one mechanism per environment, or
  • Set dotenv_override=True deliberately and own the precedence in the .env file.

File-on-disk caveat for AFFINITY_API_KEY_FILE

The SDK opens the file in three syscalls (existence check, permission stat, read). A co-tenant with write access to the parent directory could in principle swap the file between checks. This is the standard Unix file-handle race; the mitigation is to keep the key file in a directory you control (your home dir, a locked-down /etc subtree, or a Kubernetes-mounted Secret volume), not in a world-writable temp dir.

Next steps