EveryAppEveryApp

Secrets, Gateway settings and capabilities

Where an app's keys live (a Gateway setting held once for every app, the app's own secret, or a capability the Gateway brokers), how everyapp configure sets up everything an app declares and everyapp secret set NAME one name, where each name goes and what is refused, how keys work in local development, and the Postgres secrets file.

The three places a value can live

Place What it is Who sets it How an app uses it
A Gateway setting One plain secret on the Gateway Worker (every-app-gateway), held once for every app and every organization: an AI provider's key, a capability's key or OAuth client, the email sender, MCP and Cloudflare Access switches. The names are SETTABLE_GATEWAY_SETTINGS in packages/sdk/src/internal/gatewaySettings.ts, and the settings of the API providers registered on the Gateway (Registered providers). The operator, with pnpm everyapp configure <app> (everything the app needs) or pnpm everyapp secret set NAME (one name), from any directory. The Gateway reads it on its next request. Never directly. The app lists a provider or capability in everyapp.config.ts and calls through the SDK over its private EVERY_APP_GATEWAY binding; the Gateway puts the key on the call. An app's manifest may not take a Gateway name as its own secret.
The app's own secret A name the app's manifest lists under secrets, set on the app's Worker and on each of its helper Workers (<app worker>--<helper>). Whoever deploys the app, with pnpm everyapp configure <app> from anywhere, or pnpm everyapp secret set NAME from the app's directory (from anywhere with --app <id>), after its first deploy. Later deploys keep it. env.NAME in the Worker, a string.
A capability or provider the Gateway brokers Not a value the app holds at all: the Gateway holds the key or client (a Gateway setting) and performs or forwards the call. The catalog's kinds: api_key, ai (listed under providers), oauth_client, service (packages/sdk/src/internal/capabilities.ts); an API-key provider may also be registered on one Gateway (/docs/agent/providers). The operator sets the Gateway setting; the app lists the capability. createGatewayFetch() for AI providers, integrationFetch() for keys and OAuth clients, sendEmail() and listMembers() for services; integrationStatus() to ask whether it is ready.

The deploy itself also sets two secrets on every one of the app's Workers with cf deploy --secrets-file: GATEWAY_URL and EVERY_APP_ORG_ID. They are not the app's to set, and secret list hides them unless --verbose.

everyapp configure <app>

pnpm everyapp configure <app> is the one command that sets up an app (packages/cli/src/commands/configure/index.ts). It reads what the app's manifest declares (its AI providers, capabilities and own secrets) and what is set, asks for each setting that isn't, and sets everything where it belongs in one go. secret set stays for one name at a time and for scripts (below).

Named, the app is read from the registry (the manifest its last deploy stored), so the command runs from any directory; an app never deployed is refused with APP_NOT_FOUND (<app> isn't deployed to this Gateway then An app is configured by name after its first deploy; in its directory, configure works before that too (for the Gateway's settings). Nothing was changed.; rerun pnpm everyapp status); a registered row whose first deploy never finished (status: "pending") counts as undeployed, by name and in its directory. Without a name it reads the everyapp.config.ts in the current directory, deployed or not: before the first deploy only the Gateway's settings can be set, and the app's own wait for the deploy. Neither: MANIFEST_NOT_FOUND (No everyapp.config.ts in this directory, and no app named; fix name the app: pnpm everyapp configure <app> (pnpm everyapp status lists them), or run this from its directory). The Gateway is reached (login and account as every Gateway command) before anything is asked.

At a terminal

One dim line says where the values go: <app>'s settings are held as the Gateway's secrets: an AI key or a capability's client on the Gateway Worker, once for every app; <app>'s own secrets on its Worker. Changes are live at once, no deploy needed. Then the SETTING / WHERE / STATUS table secret list prints (names only, never values; before the first deploy the app's own rows read after first deploy; an app with no rows: <app> declares no secrets and needs no Gateway settings.). Then the prompts, for whatever --from-env (below) didn't cover (piped stdin means no terminal, so the lines on it never meet a prompt):

  • A hidden prompt per required setting that is unset, in manifest order, with what status says about the setting above its first name: <Label>: <why>, the provider's hint (what the key is and where it comes from), Get it at <docs> when the registry or manifest knows where, and Guide: .claude/skills/everyapp/setup/<name>.md when it has a guide (Guide: pnpm everyapp provider show <id> for a provider registered on the Gateway). A plain setting (EMAIL_FROM and the others under Plain settings are shown and checked) gets a visible prompt with Takes … above it and is checked as typed. A setting with several names (an OAuth client: GOOGLE_WORKSPACE_CLIENT_ID + GOOGLE_WORKSPACE_CLIENT_SECRET) reads unset while one name is missing, and only the missing name is asked for.
  • Which <group>? One is enough. for an unmet any-of group whose members can be set now (a menu of their labels; one member only, or one already covered: no question), then the chosen member's value. A group one member already meets asks nothing; its other members are offered with the optional ones.
  • Set the N optional settings too? (Set the optional setting too? for one), the unmet optional settings named under it, No by default: one keystroke skips them all. Yes asks for each as NAME (Enter to skip); Enter skips that one.

An app that is ready gets a menu instead, Everything <app> needs is set. Change one?, listing every setting that can be set now (NAME, or ID + SECRET for a pair, with its label and status as the hint) and No, done; a chosen setting is asked for as above, until No, done. An app that declares nothing is done at once. Ctrl-C at any prompt is CANCELLED: Cancelled. Nothing changed.

Everything is applied at the end, in one go, through the machinery secret set uses (the leases, the replaceable-version check, one cf workers secrets bulk call per Worker, the version recorded as everyapp's own, the app's secret names recorded on its row; all under Where everyapp secret set NAME puts a name): one new Worker version per destination, the Gateway's first. Steps secrets.gateway and secrets.app end Set A and B on the Gateway and Set C on <app>. Live at once; no deploy.

Flags of configure

Flag Effect
--from-env A boolean. Each unset setting's value (required, any-of members and optional alike) from the environment variable of its own name: DATAFORSEO_API_KEY from $DATAFORSEO_API_KEY. A name that is set already is never replaced (of a pair with one name set, only the other is filled; secret set NAME --from-env NAME replaces one on purpose); a variable unset or empty is skipped. Stdin isn't read, as secret set --from-env leaves it alone, so a parent that keeps the pipe open never holds the run. What it doesn't cover is asked for at a terminal. --verbose shows each <NAME> from $<NAME> (progress configure.env).
--gateway URL The Gateway to reach, as on every Gateway command.
--account-id ID The Cloudflare account.
--json NDJSON events on stdout and never a prompt: the values come from stdin or --from-env.
--verbose Which Worker each name goes to and its new version (the secrets.route and secrets.version progress lines), and what --from-env took (configure.env).

How the values get in

A value never goes on the command line. pnpm everyapp configure NAME=value is refused before anything is read, VALUE_ON_COMMAND_LINE: NAME=… looks like a value on the command line, which configure never takes then It lands in the shell's history and the process list: rotate it if it was real. Nothing was set.; fix pipe NAME=value lines in (one per line), or export NAME and add --from-env. A second argument (pnpm everyapp configure crm OPENAI_API_KEY=sk…) never reaches the command: the argument parser refuses it as TOO_MANY_ARGUMENTS, configure takes one argument, but got another: "OPENAI_API_KEY=…" then A value never goes on the command line: it lands in the shell's history and the process list. Rotate it if it was real. (the input is cut at its =); --from-env=<anything> is INVALID_VALUE, --from-env is a switch and takes no value then the same line, and what followed the = is never shown. Three sources, which don't all combine: --from-env fills what it can and the prompts cover the rest; the lines on stdin combine with nothing (piped stdin means no terminal, and with --from-env stdin isn't read):

  1. The prompts above, when a person can be asked (stdin and stderr are terminals, not --json, CI unset: canPromptInTerminal).
  2. NAME=value lines on stdin, one per line (parsePipedLines in packages/cli/src/commands/configure/index.ts): what a command produced, or a file the person wrote. Blank lines and lines starting with # are skipped; an export prefix is allowed; one pair of matching quotes (' or ") around the value is dropped; the value is otherwise taken as it is (a # or an = inside it is part of it); a later line for the same name wins. Every name given is set, set already or not; a name a capability reads once set (EMAIL_FROM_NAME, EMAIL_PROVIDER, RESEND_API_KEY and EMAIL_REST_API_TOKEN for email) is taken too, though never prompted for. Each line is checked before anything is written: a line that isn't NAME=value is PIPED_LINE_INVALID (Line <n> of the piped input isn't NAME=value then Nothing was set.; fix pipe NAME=value lines, one per line; a bare value goes to pnpm everyapp secret set NAME; the line is never echoed, since the echo "$KEY" | … habit pipes a bare secret); a name the manifest doesn't declare is SECRET_NOT_DECLARED (<app> doesn't declare NAME when the name looks like one, upper snake case; otherwise <app> doesn't declare line <n> (not shown: it may be a value); then The piped lines set what its deployed manifest lists (a provider, a capability or one of its secrets). Nothing was set., its everyapp.config.ts lists in the app's directory; fix leave it out, or declare it in everyapp.config.ts; a bare value goes to pnpm everyapp secret set NAME, by name … declare it in everyapp.config.ts and deploy again; …); a blank value is SECRET_EMPTY (NAME is empty in the piped lines; fix give NAME a value, or leave the line out); a plain setting's value the Gateway wouldn't read is SETTING_VALUE_INVALID, as from secret set; one of the app's own before its first deploy is APP_NOT_FOUND (<app> isn't deployed yet then Its own NAME can be set after its first deploy. Nothing was set.; rerun pnpm everyapp deploy).
  3. --from-env, above: the unset names only, stdin left unread.

Stdin and --from-env are how a script or a coding agent, which can't answer a prompt, gets values in. Under --json (in CI, or without a terminal) with nothing piped, no --from-env variable found and a required setting unset, the run stops with SECRET_VALUE_REQUIRED: Can't ask for <app>'s settings without a terminal then <app> needs <names>. Run this at a terminal, pipe NAME=value lines in (one per line), or export them under those names and add --from-env (which also takes the variable of any unset optional setting).; data.names the names still missing (each required one and the first member of each unmet group; of a pair with one name set, only the other). The rerun is pnpm everyapp configure <app> --from-env (rerunNote with the variables exported) once the app is deployed; before its first deploy it is pnpm everyapp configure --from-env (in the app's directory, with the variables exported), since the name form is refused for an undeployed app.

pnpm everyapp configure crm                                                        # a person, at a terminal
printf 'OPENROUTER_API_KEY=%s\n' "$OPENROUTER_API_KEY" | pnpm everyapp configure crm --json
pnpm everyapp configure crm --from-env --json                                      # every exported name at once

What it says at the end

done with message Set A, B and C. <app> is ready (the names in manifest order, whatever order they came in; <app> is ready alone with nothing to set) and data: { app: { id, deployed, setup }, set: string[], changed?: { gateway?: { names, versions }, app?: { id, workerName, names, versions } }, next? }: setup the AppSetup of /docs/agent/json, read again after the change; set the names set; changed as secret set's data, absent when nothing was set. An app still not ready gets the warning APP_NEEDS_SETUP that deploy and status give (step app.secrets, <app> won't open until <names> is set, data.setup; the names are the ones still missing, so a pair with one name set names the other), the message Set A. <app> still needs X and next: [{ command: "pnpm everyapp configure <app>", note: "at a terminal, with NAME=value lines on stdin, or with --from-env" }] (before the first deploy, { command: "pnpm everyapp configure", note: "in the app's directory: at a terminal, with NAME=value lines on stdin, or with --from-env" }). An app not deployed yet, whose Gateway settings are now set, gets no warning (its own read as unknown until its Worker exists): Set A and B. <app>'s Gateway settings are set; its own X and Y come after its first deploy with next: [{ command: "pnpm everyapp deploy", note: "then pnpm everyapp configure <app> for X and Y" }] (an app with no secrets of its own: <app>'s Gateway settings are set, note in the app's directory). A failure after one destination already changed is SECRETS_PARTIALLY_APPLIED, with the message and data: { applied, left } of secret set (below) but rerun pnpm everyapp configure <app> (rerunNote to set what is left; before the first deploy pnpm everyapp configure, in the app's directory, to set what is left), retryable: run again, it reads what is set and asks only for what is still missing.

setup.command (what status --json data.apps[].setup, secret list --json data.app.setup and the Gateway's app lists carry) is { command: "pnpm everyapp configure <app>" } while something is unmet, null once the app is ready; the Gateway's "Needs setup" dialog, deploy's next, status's APP_NEEDS_SETUP fix, secret list's next and the MCP instructions (needs setup: an admin runs pnpm everyapp configure <app>) all name it. The one exception is an MCP app (pnpm everyapp mcp on dataforseo: tier provider, no Worker, nothing to configure), whose command stays pnpm everyapp secret set DATAFORSEO_API_KEY.

Where everyapp secret set NAME puts a name

packages/cli/src/commands/secret/index.ts routes every name before it touches the network or asks for a value, so a refusal never costs a typed value. One call may mix Gateway settings and app secrets; each name goes where it belongs, the Gateway's first.

  1. The name is in SETTABLE_GATEWAY_SETTINGS, or ends in _MCP_TOKEN (a remote MCP app's credential: REMOTE_MCP_SECRET_PATTERN, "MCP apps" below): it goes to the Gateway Worker, from any directory. The manifest in the current directory is never read for it, and neither is the registry: the family is a fixed rule (isSettableGatewaySetting in packages/sdk/src/internal/gatewaySettings.ts). A name a provider registered on the Gateway holds (a row of its providers table, Registered providers) goes there too: for a name of that shape the command connects first to ask the rows, prints whose it is and the hosts its key is sent to, and after setting it binds the row.
  2. Any other name needs the app's manifest: everyapp.config.ts in the current directory (cwd), or with --app <id> the manifest the registry stored at the app's last deploy (given both, --app wins and the directory is never read). If that manifest lists the name under secrets, it goes to the app's Worker and each helper Worker. (unset also takes a name the manifest no longer lists, so a secret dropped from the manifest can still be removed; such names are reported as "NAME isn't listed in everyapp.config.ts." above the confirmation.)
  3. Otherwise the call is refused, in this order:
Condition Code Message (first line) and next step
A known name (a Gateway setting or a declared secret) differs only in case or underscores SECRET_NOT_DECLARED NAME isn't a Gateway setting (in an app's directory: … or one of <id>'s secrets); data.didYouMean holds the match, rerun is the same command with it substituted.
An install-owned Gateway name (INSTALL_OWNED_GATEWAY_SETTINGS), outside an app's directory SECRET_NOT_SETTABLE NAME can't be changed with secret set then It's one of the Gateway's own keys, written once by setup.; for THIRD_PARTY_DEPLOYS: It's the hosted Gateway's launch gate, opened by hand.
A dev-only Gateway name (MCP_ACCESS_TOKEN_TTL, DATAFORSEO_SANDBOX) SECRET_NOT_SETTABLE NAME only works in local development then A deployed Gateway doesn't read it.; fix: set it in the Gateway's .dev.vars.
Not an identifier (^[A-Za-z_][A-Za-z0-9_]*$) SECRET_NAME_INVALID "NAME" isn't a valid secret name; fix: use letters, digits and underscores, starting with a letter, e.g. GITHUB_TOKEN.
A reserved app-secret name (below) SECRET_NAME_RESERVED NAME is a reserved name then everyapp manages GATEWAY_URL, CF_VERSION_METADATA and the EVERYAPP_*, EVERY_APP_* and APP__* names.; fix: pick another name.
No everyapp.config.ts in the current directory SECRET_NOT_DECLARED NAME isn't a Gateway setting, and there's no app here, then An app's own secrets are set from the app's directory (where everyapp.config.ts is), or from anywhere with --app <id>. and Gateway settings: pnpm everyapp secret list; fix: cd into the app's directory and run this again, or add --app <id>.
set: the manifest doesn't list the name SECRET_NOT_DECLARED <id> doesn't list NAME as a secret then It isn't a Gateway setting either. (for an install-owned name: It's one of the Gateway's own keys; listing it under secrets gives <id> a secret of its own.); fix: add it to secrets in everyapp.config.ts: secrets: [...],; rerun: pnpm everyapp secret set NAME.
  1. The CLI then connects to the Gateway (login and account as every Gateway command; --gateway, --account-id) and, for app names, finds the app in the registry. An app that was never deployed is refused with APP_NOT_FOUND: <id> isn't deployed yet then Its secrets can be set after its first deploy. Nothing was changed.; rerun pnpm everyapp deploy.
  2. Only now is each name's value read (one prompt per name, in the order given).
  3. The change is applied under leases (gateway.secrets, then app.secrets; acquireGatewayLease and acquireAppLease in packages/cli/src/lib/gateway/lease.ts). While another run holds one (a deploy, setup, email set, or another secret change), the command waits up to 5 minutes, saying so as progress on gateway.lease or app.lease (Waiting for a change to the Gateway's settings to finish (<who> on <machine>, started 1m ago), Waiting for a secret change on <id> to finish (…)), then fails with LEASE_HELD before anything changes: A change to the Gateway's settings is still running after 5m or A secret change on <id> is still running after 5m (roughly() in packages/cli/src/lib/output/time.ts renders the wait as 5m), then It's from <who> on <machine>. If it stopped without cleaning up, it's released within <time>.; retryable, data.holder, rerun once it's done (pnpm everyapp status shows it). The command never calls lease.check(), so it doesn't raise LEASE_LOST; a heartbeat renews each lease every 30 seconds until it is released. For the Gateway, the install row must exist (GATEWAY_NOT_INSTALLED, rerun pnpm everyapp setup). Every target Worker is checked first (assertReplaceable in packages/cli/src/lib/cf/deploy.ts): if the version serving is not one everyapp recorded, an unattended run stops with action_required (rerun_interactive: "… was changed outside everyapp; confirm in a terminal") and a person is asked "Set NAME on it anyway?". Then one cf workers secrets bulk --worker <name> --body @<file> call per Worker makes a new version with the change (packages/cli/src/lib/cf/secrets.ts): the Gateway's Worker, then the app's Worker, then each helper. The new version id is recorded as everyapp's own (the install row's live_version_id, or the app's resource records), so the next deploy or setup replaces it unattended. No deploy happens; the secret is live at once.

Events: progress secrets.route (NAME is a Gateway setting → Worker every-app-gateway or NAME is <id>'s secret → Worker <app>, <helper>), step secrets.gateway and secrets.app (message Updating the Gateway... / Updating <id>..., then Set NAME on the Gateway; data: { action, names, versions }), progress secrets.version (<worker> is now at version <id>), and done with message Set OPENAI_API_KEY on the Gateway, and GITHUB_TOKEN on todo and data: { gateway?: { names, versions, pinned? }, app?: { id, workerName, names, versions } } (versions maps each Worker name to its new version id; pinned lists each remote MCP credential set, { name, origin }, with the one origin the Gateway now sends it to, and the message and the display add <NAME> is sent to <origin> and nowhere else. per credential). A remote MCP credential adds progress secrets.pin (<NAME> is pinned to <origin>) and may warn REMOTE_MCP_SECRET_UNDECLARED or REMOTE_MCP_SECRET_AMBIGUOUS ("MCP apps" below).

A failure after one destination already changed is SECRETS_PARTIALLY_APPLIED (retryable): Couldn't set NAME on <destination>, Cloudflare said: …, X is set; only Y is left., with rerun naming only what is left ( --app <id> added when the command ran by --app and a name of the app's own is left) and data: { applied, left }, so no value is asked for twice. Raised through configure, the same message and data come with rerun pnpm everyapp configure <app> (rerunNote to set what is left; before the first deploy pnpm everyapp configure, in the app's directory, to set what is left).

Flags of secret set

Flag Effect
--from-env VAR Read the value from the environment variable VAR. One name per call.
--app <id> The app whose secret this is, by its id, with its manifest read from the Gateway's registry, so the command runs from any directory (default: the app in this directory; given both, --app wins). secret unset takes it too. A name the stored manifest doesn't list is refused (SECRET_NOT_DECLARED); a row whose stored manifest doesn't parse answers INVALID_MANIFEST.
--gateway URL The Gateway to reach, as on every Gateway command.
--account-id ID The Cloudflare account.
--json NDJSON events on stdout and never a prompt: the value must come from --from-env or stdin.
--verbose Show which Worker each secret goes to and its new version (the secrets.route and secrets.version progress lines).

How a value is read: never the command line

packages/cli/src/lib/secretValue.ts takes a value from exactly one source, decided before anything else is checked:

Source When Rule
--from-env VAR The flag is given process.env[VAR]; empty or unset is SECRET_EMPTY ($VAR is empty or not set; fix: export VAR first, or name another variable with --from-env). This is the form for agents and CI. A plain setting's value (below) is trimmed and checked: SETTING_VALUE_INVALID.
stdin No --from-env and stdin is not a terminal (piped) The whole input, one trailing newline dropped (echo "$STRIPE_KEY" | pnpm everyapp secret set STRIPE_KEY). Empty input is SECRET_VALUE_REQUIRED. A plain setting's value is trimmed and checked: SETTING_VALUE_INVALID.
A hidden prompt No --from-env, stdin is a terminal, a credential Value for NAME, typed invisibly (no length shown), echoed as a fixed mask, trimmed. Asked only when a person can be asked: stdin and stderr are terminals, not --json, and CI is not set in the environment (canPromptInTerminal in packages/cli/src/lib/output/prompt.ts).
A visible prompt No --from-env, stdin is a terminal, a plain setting NAME as the question under a dim line Takes … saying what it takes (Takes an address, or Name <address> (with EMAIL_FROM_NAME set, unless the name is in it)); typed and echoed in the clear, trimmed; an answer the Gateway wouldn't read is re-asked with the reason (Not one of true, false, 1, 0, on, off, yes or no.). Same conditions as the hidden prompt.

With --json, in CI, or without a terminal, and no --from-env or piped value, the command fails with SECRET_VALUE_REQUIRED: Can't ask for NAME's value without a terminal then Or pipe it in: echo "$NAME" | pnpm everyapp secret set NAME; rerun pnpm everyapp secret set NAME --from-env NAME.

--from-env and a piped value hold one value, so naming several secrets with either is SECRET_SEVERAL_NAMES (--from-env sets one secret at a time, and you named 2; fix: run it once per name). The prompt asks once per name, so pnpm everyapp secret set LINKEDIN_CLIENT_ID LINKEDIN_CLIENT_SECRET works at a terminal.

Plain settings are shown and checked

A credential is never shown and never checked. A plain Gateway setting, one of PLAIN_GATEWAY_SETTINGS (isPlainGatewaySetting in packages/sdk/src/internal/gatewaySettings.ts: EMAIL_FROM, EMAIL_FROM_NAME, EMAIL_PROVIDER, EMAIL_PASSWORD_SIGN_IN, MASTODON_URL, AI_PROXY_EXTRA_ROUTES, the MCP_* switches and lists, the CLOUDFLARE_ACCESS_* names other than the OIDC client secret, HOSTED_OPEN_SIGNUP, BROWSER_APP_INSTALLS, DISPATCH_NAMESPACE, THIRD_PARTY_DOMAIN, EVENTS_ALLOW_PRIVATE_TARGETS), is a word, an address, a URL or a list rather than a key, so secret set shows it as it is typed and, from every source, trims it and checks it before anything is written, the way the Gateway reads it (PLAIN_SETTINGS in packages/cli/src/lib/secretValue.ts; the Gateway settings tables below say the same per name):

  • EMAIL_PASSWORD_SIGN_IN, MCP_OAUTH_ENABLED, MCP_DCR_ENABLED: true or false, any case; 1, 0, on, off, yes and no are honoured too. The prompt says which one turns the feature off (false). HOSTED_OPEN_SIGNUP: true or false. BROWSER_APP_INSTALLS: on or off, in lowercase (the Gateway compares it as typed). EVENTS_ALLOW_PRIVATE_TARGETS: true or false, in lowercase (the Gateway turns it on for exactly true). CLOUDFLARE_ACCESS_OUTER_RING_AUTO_JOIN: everyone; to turn it off, pnpm everyapp secret unset CLOUDFLARE_ACCESS_OUTER_RING_AUTO_JOIN.
  • EMAIL_FROM: an address, or Name <address> (parseEmailSender). EMAIL_PROVIDER: cloudflare or resend, any case.
  • MASTODON_URL: an https origin with no path (oauthClientOrigin; a bare host is taken as https). MCP_ALLOWED_ORIGINS: http or https URLs, comma or space separated. CLOUDFLARE_ACCESS_OIDC_ISSUER: https://<team>.cloudflareaccess.com/cdn-cgi/access/sso/oidc/<client id>. CLOUDFLARE_ACCESS_TEAM_DOMAIN: <team>.cloudflareaccess.com, with or without https://. Both as the Gateway's accessUrl takes them: user info, a query or a fragment in the URL is refused.
  • CLOUDFLARE_ACCESS_AUTO_JOIN_DOMAINS: domains, comma or space separated (a leading @ is fine); THIRD_PARTY_DOMAIN: one domain, as the Gateway's thirdPartyDomain reads it (a trailing . is dropped; no @). CLOUDFLARE_ACCESS_AUD, CLOUDFLARE_ACCESS_APPS_AUD: AUD tags. AI_PROXY_EXTRA_ROUTES: /v1/... paths. A list with an entry of the wrong shape, or with none, is refused.
  • MCP_AUDIT_RETENTION_DAYS: a whole number from 1 to 3650.
  • EMAIL_FROM_NAME, CLOUDFLARE_ACCESS_OIDC_CLIENT_ID, CLOUDFLARE_ACCESS_OIDC_LABEL, DISPATCH_NAMESPACE: anything but nothing; the prompt still says what each is for.

At the prompt a refused answer is asked again with the reason. From --from-env or stdin it is SETTING_VALUE_INVALID: NAME can't be "<value>" (the value as typed, on one line, at most 64 characters) then the reason, with data.name and a fix saying what the setting takes (use true or false; false turns password sign-in off (only while Cloudflare Access sign-in is on)). Nothing is written. Under --json nothing else changes: there is still no prompt, and a value that passes is set as before.

A value never appears on cf's command line: patchWorkerSecrets writes a JSON merge patch ({ "secrets": { "NAME": { "name", "type": "secret_text", "text" } } }, null to remove) to a file in a private temp directory (mode 0600, removed afterwards; packages/cli/src/lib/cf/runner.ts) and passes --body @<file>. The first deploy's GATEWAY_URL and EVERY_APP_ORG_ID travel the same way, in cf deploy --secrets-file. Do not put a value in a chat message or a shell argument: export it and use --from-env.

secret unset

pnpm everyapp secret unset NAME… routes names as set does (an undeclared name still goes to the app). Before asking, it checks whether any target Worker holds any of the names; none does, and the result is done with NAME isn't set on the Gateway or <id>. Nothing changed. and data: { removed: false }. Otherwise it asks Remove NAME from the Gateway, and OTHER from <id>? (No by default). --yes (-y) skips the question; without a terminal (--json, CI) and without --yes it fails with CONFIRMATION_REQUIRED (Removing secrets needs confirmation, and there's no terminal to ask in), whose rerun adds --yes and is marked authorize: show it and ask before running it.

Under the leases, each target keeps only the names it holds (a merge patch of null for an absent name would make a version with no change); --verbose shows NAME isn't set on <worker> (progress secrets.held). The done message is Removed NAME from the Gateway, and OTHER from <id>, Live now. No deploy needed., with the same data shape as set. Unsetting a remote MCP credential also deletes its pin (progress secrets.pin: <NAME>'s pin to its MCP server's origin is gone).

secret list

pnpm everyapp secret list [<app>] prints names, never values. Flags: --gateway, --account-id, --json, --verbose. The optional <app> is an app id: the table for that app from any directory (its manifest from the registry); without it, the app in this directory, or outside one the Gateway's table and then every registered app's settings.

Outside an app's directory it lists the Gateway Worker's secrets as a NAME / SET BY table under Gateway secrets · <host>: every name that is not install-owned as you, each install-owned name present as setup, and of the settable names not set only those some registered app's manifest reads (hosted-only names only on a hosted install), the rest as one dim line, N more can be set. Then each registered app's settings table (below). Each remote MCP credential a live MCP app declares and the Gateway doesn't hold is listed among the unset names, and each MCP app that needs setup (its key or credential unset) gets a settings table of its own with the one secret set that sets it up; MCP apps that are ready are left out (pnpm everyapp mcp list shows them all). The done event's data is { gateway: { workerName: "every-app-gateway", names, settable, mcpCredentials }, apps: [{ id, setup }] } (names: the Worker's secrets, sorted; settable: SETTABLE_GATEWAY_SETTINGS; mcpCredentials: [{ name, apps, set }], each remote MCP credential name with the ids of the live apps that declare it and whether it is set; setup each app's AppSetup, below).

For one app (the app in this directory, or <app>) it is a SETTING / WHERE / STATUS table (settingsRows and renderSettingsTable in packages/cli/src/lib/app/settings.ts), one row per setting the manifest declares, in manifest order: the Gateway settings its providers and capabilities need (WHERE Gateway; an OAuth client's pair joined with +), then the app's own secrets (WHERE the app id), then each helper Worker's view of the app's own secrets (WHERE <helper> helper); reserved names (GATEWAY_URL, EVERY_APP_ORG_ID) only with --verbose.

  • STATUS is the SETTINGS vocabulary status shares (settingStatus): ✓ set, ! not set (required, or every member of an unmet any-of group), not set (optional), not set (one of: <group>) when another member is set, ! not recorded for an app secret whose names the CLI never recorded, ! couldn't check when cf couldn't list a Worker. Before the first deploy the app's own rows read after first deploy, and the note <id> isn't deployed yet; its own secrets can be set after that. closes the output.
  • Under the table, when something is unmet, each unmet setting as status describes it (describeUnmetSettings): <label>: <why> (the names when the manifest gives no why and the label doesn't name them), the provider's hint (what the key is and where it comes from, from its entry), Get it at <docs> when the registry or manifest knows where, and Guide: .claude/skills/everyapp/setup/<name>.md once per distinct guide; a provider registered on the Gateway has no guide file, and its line is Guide: pnpm everyapp provider show <id>. --json carries the same as each setting's hint and setup.
  • No rows at all: <id> declares no secrets and needs no Gateway settings. (deployed or not).

Its data: { app: { id, deployed, workerName, declared, workers: [{ workerName, names, missing }], secretNames, settings, setup }, gateway: { workerName, names, needed: [{ name, required, set }], missing, settable }, next? }. secretNames is the app Worker's own names as cf listed them, reserved names dropped (null before the first deploy); settings is every setting with its state and setup the AppSetup summary (/docs/agent/json); needed[].required is true for a required setting or an any-of member. next mixes { command, note } objects and command strings: once deployed, pnpm everyapp configure <app> (setup.command.command) while something is unmet; before the first deploy, { command: "pnpm everyapp configure", note: "in the app's directory" } while something the Gateway holds is unmet, then pnpm everyapp deploy (alone when nothing is); absent for a deployed app that is ready, or an app with no rows.

Gateway settings

Every name pnpm everyapp secret set changes on the Gateway, from SETTABLE_GATEWAY_SETTINGS (packages/sdk/src/internal/gatewaySettings.ts) and the remote MCP credentials' _MCP_TOKEN family ("MCP apps" below), with what it configures (apps/every-app-gateway/src/env.d.ts and the modules named) and the manifest entry an app lists to use it. "Required" and "optional" are what the settings model reports for an app that lists the entry (appSettings() in packages/sdk/src/internal/appSettings.ts: required unless the entry says optional: true or names an anyOf group): required ones show as ! not set in secret list, optional ones as not set (optional).

Where a setting has a setup skill, its section names it as setup/<name>.md: that is packages/cli/skills/everyapp/setup/<name>.md in the EveryApp checkout and, in an app, .claude/skills/everyapp/setup/<name>.md (also under .agents/skills/everyapp/setup/; everyapp create copies the skill into both, packages/cli/src/lib/app/skill.ts); the index is setup/README.md. The skills hold the step-by-step setup of each integration; this page holds what each setting does.

AI providers

An app lists providers: ["openai"] and calls /api/ai/<provider>/... with createGatewayFetch(); ai.models and ai.routes in the manifest limit what it may ask for. Step by step: setup/openai.md, setup/openrouter.md, setup/workers-ai.md. The table is generated from the catalog's ai entries (/docs/agent/providers); a provider whose key (or, for Workers AI, the account) is unset answers ai_provider_not_configured.

Setting Configures Manifest entry
OPENAI_API_KEY OpenAI (api.openai.com), the key sent as Authorization: Bearer <OPENAI_API_KEY>. A secret key from the API keys page; the Gateway puts it on every call as a bearer token. providers: ["openai"], or ai: { chat: true } when it is the Gateway's default
OPENROUTER_API_KEY OpenRouter (openrouter.ai), the key sent as Authorization: Bearer <OPENROUTER_API_KEY>. An API key from OpenRouter's keys page; the Gateway puts it on every call as a bearer token. providers: ["openrouter"], or ai: { chat: true } when it is the Gateway's default
WORKERS_AI_API_TOKEN, CLOUDFLARE_ACCOUNT_ID (set by everyapp setup) Workers AI (api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai), the key sent as Authorization: Bearer <WORKERS_AI_API_TOKEN>. A Cloudflare API token with Workers AI Read (the Workers AI page's prefilled token); the account is the Gateway's own. providers: ["workers-ai"], or ai: { chat: true } when it is the Gateway's default
AI_GATEWAY_API_KEY, CLOUDFLARE_ACCOUNT_ID (set by everyapp setup), AI_GATEWAY_ID Cloudflare AI Gateway (gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{AI_GATEWAY_ID}), the key sent as cf-aig-authorization: Bearer <AI_GATEWAY_API_KEY>. The gateway's id from the AI Gateway page (everyapp ai gateway on makes one named everyapp); AI_GATEWAY_API_KEY only for an authenticated gateway, sent as cf-aig-authorization. AI_GATEWAY_ID: The AI Gateway's id (its name in the dashboard), in the Gateway's own Cloudflare account. AI_GATEWAY_API_KEY: An AI Gateway token with Run permission, only when the gateway is authenticated; leave unset for an open gateway. never listed: pnpm everyapp ai gateway on turns it on and every AI provider with an AI Gateway slug goes through it
ANTHROPIC_API_KEY Anthropic (api.anthropic.com), the key sent as Authorization: Bearer <ANTHROPIC_API_KEY>. An API key from the Claude Console's API keys page; the Gateway sends it as a bearer token. Apps send the anthropic-version header themselves. A key scoped to one workspace; the Gateway strips any anthropic-workspace-id header an app sends, so the key alone decides the workspace. providers: ["anthropic"], or ai: { chat: true } when it is the Gateway's default
GEMINI_API_KEY Google Gemini (generativelanguage.googleapis.com), the key sent as Authorization: Bearer <GEMINI_API_KEY>. An API key from Google AI Studio; the Gateway sends it as a bearer token. Apps call Gemini's OpenAI-shaped paths under /v1beta/openai. A Google AI Studio key restricted to the Gemini API only. providers: ["gemini"], or ai: { chat: true } when it is the Gateway's default
MISTRAL_API_KEY Mistral AI (api.mistral.ai), the key sent as Authorization: Bearer <MISTRAL_API_KEY>. An API key from API Keys in the Mistral Studio console; the Gateway sends it as a bearer token. Free mode needs no card; usage and rate limits apply. A key from API Keys in the Studio console, with an expiry date. providers: ["mistral"], or ai: { chat: true } when it is the Gateway's default
GROQ_API_KEY Groq (api.groq.com), the key sent as Authorization: Bearer <GROQ_API_KEY>. An API key from the Groq console; the Gateway sends it as a bearer token. Apps call the OpenAI-shaped paths under /openai/v1. A key from the API Keys page of the Groq console. providers: ["groq"], or ai: { chat: true } when it is the Gateway's default
COHERE_API_KEY Cohere (api.cohere.com), the key sent as Authorization: Bearer <COHERE_API_KEY>. An API key from the Cohere dashboard; the Gateway sends it as a bearer token. Trial keys are free and limited; production keys are paid. A production key; a trial key is limited to 1,000 calls a month. providers: ["cohere"]

One more AI setting is the Gateway's own: AI_PROXY_EXTRA_ROUTES, the OpenAI paths beyond inference apps may opt into (comma or space separated, each matching ^/v1(/[a-z0-9_-]+)+$, e.g. /v1/files; each covers its sub-paths). A path is allowed only when both this and the app's ai.routes list it (apps/every-app-gateway/src/server/ai-proxy.ts); the manifest entry is ai: { routes: [...] }, required when routes is non-empty.

API-key capabilities

An app lists capabilities: { <id>: { access: "read", why } } and calls the provider's real URLs with integrationFetch({ env, provider: "<id>", for }); the Gateway puts the key on the call and forwards it to the provider's hosts only. Step by step: setup/dataforseo.md, setup/context.md, and for every catalog provider setup/providers.md#<id>. The same DATAFORSEO_API_KEY also serves DataForSEO's tools straight to members' AI clients as an MCP app, with no page and nothing to deploy: pnpm everyapp mcp on dataforseo (/docs/agent/mcp). The table is generated from the catalog's api_key entries:

Setting Configures Manifest entry
DATAFORSEO_API_KEY DataForSEO (api.dataforseo.com, sandbox.dataforseo.com), the key sent as Authorization: Basic <DATAFORSEO_API_KEY>, the value login:password base64-encoded (or sent as it is when it holds no colon: already encoded). The API login and password from API Access, as login:password (or that pair base64-encoded); the Gateway sends them as HTTP Basic. login:password, or its base64; never the dashboard sign-in password. capabilities: { dataforseo }
CONTEXT_API_KEY Context.dev (api.context.dev), the key sent as Authorization: Bearer <CONTEXT_API_KEY>. An API key from the Context.dev dashboard; the Gateway puts it on every call as a bearer token. capabilities: { context }
STRIPE_API_KEY Stripe (api.stripe.com, files.stripe.com), the key sent as Authorization: Bearer <STRIPE_API_KEY>. A restricted API key from the API keys page of the Stripe dashboard, with only the permissions the app needs. The Gateway sends it as a bearer token. A restricted key (rk_live_...) with only the permissions the app needs; a sandbox key (rk_test_... or sk_test_...) to try it. capabilities: { stripe }
LEMONSQUEEZY_API_KEY Lemon Squeezy (api.lemonsqueezy.com), the key sent as Authorization: Bearer <LEMONSQUEEZY_API_KEY>. An API key from Settings > API in Lemon Squeezy; the Gateway sends it as a bearer token. The API is JSON:API, so the app sends Accept and Content-Type application/vnd.api+json. An API key from Settings > API; a test mode key reaches test mode store data only. capabilities: { lemonsqueezy }
PADDLE_API_KEY Paddle (api.paddle.com, sandbox-api.paddle.com), the key sent as Authorization: Bearer <PADDLE_API_KEY>. An API key from Paddle > My account > Settings > Authentication (API keys tab), with only the permissions the app needs; the Gateway sends it as a bearer token. Live and sandbox are separate hosts with separate keys. An API key (pdl_live_apikey_... or pdl_sdbx_apikey_...) with only the permissions the app needs; a sandbox key works on the sandbox host only. capabilities: { paddle }
SHOPIFY_API_TOKEN, SHOPIFY_HOST Shopify (the host <SHOPIFY_HOST> names (a bare host or an https origin)), the key sent as X-Shopify-Access-Token: <SHOPIFY_API_TOKEN>. An Admin API access token from a custom app created in the store's admin, sent in the X-Shopify-Access-Token header to the store's myshopify.com host. Shopify no longer lets stores create new admin custom apps, and Dev Dashboard tokens expire after 24 hours, which the Gateway can't refresh yet. SHOPIFY_API_TOKEN: The Admin API access token of a custom app made in the store's admin (Apps > Develop apps > the app > API credentials); it is shown once. SHOPIFY_HOST: The store's own host, like your-store.myshopify.com. capabilities: { shopify }
PLAID_SECRET Plaid (sandbox.plaid.com, production.plaid.com), the key sent as PLAID-SECRET: <PLAID_SECRET>. Plaid needs a client_id and a secret on every call, in the JSON body or in the PLAID-CLIENT-ID and PLAID-SECRET headers. The Gateway puts on one header, so the app would have to send PLAID-CLIENT-ID itself. Every endpoint is a POST. The secret for one environment (Sandbox or Production) from the Keys page of the Plaid Dashboard. capabilities: { plaid }
ATTIO_API_KEY Attio (api.attio.com), the key sent as Authorization: Bearer <ATTIO_API_KEY>. An access token a workspace admin makes under Workspace settings > Developers, scoped to what the app needs; the Gateway sends it as a bearer token. Listing and searching records are POSTs, so an app that lists records needs write access. An access token from Workspace settings > Developers, with only the scopes the app needs. capabilities: { attio }
HUBSPOT_API_TOKEN HubSpot (api.hubapi.com), the key sent as Authorization: Bearer <HUBSPOT_API_TOKEN>. An access token from a HubSpot legacy private app (Development > Legacy apps > Create legacy app > Private), scoped to what the app needs; the Gateway sends it as a bearer token. CRM search is a POST, so an app that searches needs write access. The access token of a legacy private app (Development > Legacy apps > the app > Auth), with only the scopes the app needs. capabilities: { hubspot }
PIPEDRIVE_API_TOKEN Pipedrive (api.pipedrive.com), the key sent as x-api-token: <PIPEDRIVE_API_TOKEN>. A personal API token from Settings > Personal preferences > API in Pipedrive; the Gateway sends it in the x-api-token header. If API is missing there, an admin enables it for the person's permission set. Your personal API token from Settings > Personal preferences > API; it acts as you, and each company you belong to has its own. capabilities: { pipedrive }
AIRTABLE_API_TOKEN Airtable (api.airtable.com), the key sent as Authorization: Bearer <AIRTABLE_API_TOKEN>. A personal access token from airtable.com/create/tokens, limited to the scopes and bases the app needs; the Gateway sends it as a bearer token. Listing records by POST (listRecords) needs write access; the GET form doesn't. A personal access token limited to the scopes and bases the app needs. capabilities: { airtable }
NOTION_API_TOKEN Notion (api.notion.com), the key sent as Authorization: Bearer <NOTION_API_TOKEN>. An internal connection's API token (a workspace owner creates the connection in Notion's developer portal), sent as a bearer token; it reaches only pages shared with it. The app sends the Notion-Version header itself. Search and data source queries are POSTs, so an app that queries needs write access. The API token on an internal connection's Configuration tab; share the pages it should reach with the connection. capabilities: { notion }
POSTMARK_API_TOKEN Postmark (api.postmarkapp.com), the key sent as X-Postmark-Server-Token: <POSTMARK_API_TOKEN>. A Server API token from the API Tokens tab of the Postmark server the app sends from; the Gateway sends it in the X-Postmark-Server-Token header. A Server API token from the server's API Tokens tab, never the Account API token. capabilities: { postmark }
SENDGRID_API_KEY SendGrid (api.sendgrid.com, api.eu.sendgrid.com), the key sent as Authorization: Bearer <SENDGRID_API_KEY>. An API key from Settings > API Keys in SendGrid, sent as a bearer token; Custom Access with Mail Send alone is enough to send. EU regional subusers call api.eu.sendgrid.com. A Custom Access key with Mail Send only, never a Full Access key. capabilities: { sendgrid }
TWILIO_API_SECRET, TWILIO_API_KEY Twilio (api.twilio.com), the key sent as Authorization: Basic base64(<TWILIO_API_KEY>:<TWILIO_API_SECRET>). A Standard or Restricted API key from Account settings > API keys & auth tokens in the Twilio Console, sent as HTTP Basic (the key's SID and secret); a Standard key can't reach the Accounts or Keys resources. Every path names the account, so the app puts its Account SID in it: /2010-04-01/Accounts//Messages.json, with a form-encoded body. TWILIO_API_SECRET: The API key's secret, shown once when the key is created. TWILIO_API_KEY: The SID of a Standard or Restricted API key, never a Main key or the Auth Token. capabilities: { twilio }
INTERCOM_API_TOKEN Intercom (api.intercom.io, api.eu.intercom.io, api.au.intercom.io), the key sent as Authorization: Bearer <INTERCOM_API_TOKEN>. The Access Token of a private app in your workspace's Developer Hub, sent as a bearer token. Searching contacts, conversations or tickets is a POST, so an app that searches asks for write. The Access Token on your private app's Configure > Authentication page in the Developer Hub. capabilities: { intercom }
CRISP_API_KEY, CRISP_API_IDENTIFIER Crisp (api.crisp.chat), the key sent as Authorization: Basic base64(<CRISP_API_IDENTIFIER>:<CRISP_API_KEY>). A website token, which the workspace owner generates under Settings > Workspace Settings > Advanced configuration > API Token. The Gateway sends the identifier and key as HTTP Basic; the app sends X-Crisp-Tier: website on every call. CRISP_API_KEY: The token's key, shown once when the workspace owner generates it. CRISP_API_IDENTIFIER: The token's identifier, shown beside the key. capabilities: { crisp }
LINEAR_API_KEY Linear (api.linear.app), the key sent as Authorization: <LINEAR_API_KEY>. A personal API key from Settings > Account > Security & access; the Gateway sends it as the Authorization header with no Bearer word. Every call is a POST to /graphql, queries included, so the app asks for write. A personal API key, restricted to the permissions and teams the app needs. capabilities: { linear }
GITHUB_API_TOKEN GitHub (api.github.com), the key sent as Authorization: Bearer <GITHUB_API_TOKEN>. A fine-grained personal access token from Settings > Developer settings, limited to one owner, the repositories the app needs and the least permissions; sent as a bearer token. GraphQL is a POST to /graphql, so it needs write. A fine-grained personal access token limited to the repositories and permissions the app needs. capabilities: { github }
GITLAB_API_TOKEN GitLab (gitlab.com), the key sent as PRIVATE-TOKEN: <GITLAB_API_TOKEN>. A personal, project or group access token from GitLab.com, sent in the PRIVATE-TOKEN header; read_api is enough to read. Paths start with /api/v4. A self-managed GitLab needs its own registered provider. A personal, project or group access token with read_api, or api when the app writes. capabilities: { gitlab }
VERCEL_API_TOKEN Vercel (api.vercel.com), the key sent as Authorization: Bearer <VERCEL_API_TOKEN>. An access token from your account's Tokens page, sent as a bearer token; scope it to one team or one project. A team- or project-scoped token needs no teamId parameter. An access token scoped to one team or one project, never Full Account. capabilities: { vercel }
CLOUDFLARE_PROVIDER_API_TOKEN Cloudflare (api.cloudflare.com), the key sent as Authorization: Bearer <CLOUDFLARE_PROVIDER_API_TOKEN>. An API token from My Profile > API Tokens with only the permissions the app needs; the Gateway sends it as a bearer token. A token on the Gateway's own account can read and change the Gateway, so keep it narrow or use another account. A user or account-owned API token scoped to the zones and permissions the app needs, never a Global API Key. capabilities: { cloudflare }
POSTHOG_PERSONAL_API_KEY PostHog (us.posthog.com, eu.posthog.com), the key sent as Authorization: Bearer <POSTHOG_PERSONAL_API_KEY>. A personal API key from Settings > Personal API keys, with only the scopes the app needs; the Gateway sends it as a bearer token to us.posthog.com or eu.posthog.com. HogQL queries are POSTs, so an app that runs them asks for write. A personal API key (phx_...) with only the scopes the app needs, from the region the project lives in; never the project token. capabilities: { posthog }
MIXPANEL_SERVICE_ACCOUNT_SECRET, MIXPANEL_SERVICE_ACCOUNT_USERNAME Mixpanel (mixpanel.com, eu.mixpanel.com, in.mixpanel.com, data.mixpanel.com, data-eu.mixpanel.com, data-in.mixpanel.com, api.mixpanel.com, api-eu.mixpanel.com, api-in.mixpanel.com), the key sent as Authorization: Basic base64(<MIXPANEL_SERVICE_ACCOUNT_USERNAME>:<MIXPANEL_SERVICE_ACCOUNT_SECRET>). A service account from Organization Settings > Service Accounts; the Gateway sends its username and secret as HTTP Basic. Most calls also need the project_id parameter, and an EU or India project uses its own hosts. MIXPANEL_SERVICE_ACCOUNT_USERNAME: The service account's username, shown when it is created in Organization Settings > Service Accounts. MIXPANEL_SERVICE_ACCOUNT_SECRET: The service account's secret, shown once when it is created; give the account the narrowest role and only the projects the app needs. capabilities: { mixpanel }
SEGMENT_WRITE_KEY Segment (api.segment.io, events.eu1.segmentapis.com), the key sent as Authorization: Basic base64(<SEGMENT_WRITE_KEY>:). The write key of an HTTP API source, from the source's Settings > API Keys; the Gateway sends it as the Basic username with an empty password. It only sends events; an EU workspace uses events.eu1.segmentapis.com. The write key of an HTTP API source, from the source's Settings > API Keys. capabilities: { segment }
SUPABASE_SECRET_KEY, SUPABASE_HOST Supabase (the host <SUPABASE_HOST> names (a bare host or an https origin)), the key sent as apikey: <SUPABASE_SECRET_KEY>. A secret key (sb_secret_...) from Settings > API Keys and the project's host; the Gateway sends the key in the apikey header. The key bypasses Row Level Security, and RPC calls are POSTs, so they need write. SUPABASE_SECRET_KEY: A secret key (sb_secret_...) from Settings > API Keys, made for this Gateway alone; it bypasses Row Level Security. SUPABASE_HOST: The project's host, .supabase.co, from the project URL. capabilities: { supabase }
TURSO_API_TOKEN Turso (api.turso.tech), the key sent as Authorization: Bearer <TURSO_API_TOKEN>. An organization-scoped Platform API token (turso auth api-tokens mint --org ); the Gateway sends it as a bearer token to api.turso.tech. It manages databases and cannot query them: that is the turso-db entry. An organization-scoped or group-scoped Platform API token from turso auth api-tokens mint --org ; never an unrestricted one. capabilities: { turso }
TURSO_DB_AUTH_TOKEN, TURSO_DB_HOST Turso database (the host <TURSO_DB_HOST> names (a bare host or an https origin)), the key sent as Authorization: Bearer <TURSO_DB_AUTH_TOKEN>. A database token (turso db tokens create ) and the database's HTTP host; the Gateway sends the token as a bearer token. Every query is a POST to /v2/pipeline, so an app that queries asks for write; a --read-only token keeps it to reads. TURSO_DB_AUTH_TOKEN: A database token from turso db tokens create , with --read-only or fine-grained permissions when the app only reads. TURSO_DB_HOST: The database's HTTP host, -.turso.io, from turso db show --http-url. capabilities: { turso-db }
NEON_API_KEY Neon (console.neon.tech), the key sent as Authorization: Bearer <NEON_API_KEY>. A project-scoped or organization API key from Settings > API keys; the Gateway sends it as a bearer token to console.neon.tech. It manages projects and branches; the database itself takes Postgres credentials, not this key. A project-scoped or organization API key from Settings > API keys; a project-scoped key reaches one project only. capabilities: { neon }
PINECONE_API_KEY, PINECONE_HOST Pinecone (api.pinecone.io, the host <PINECONE_HOST> names (a bare host or an https origin)), the key sent as Api-Key: <PINECONE_API_KEY>. An API key from the project's API keys page and the host of the index the app uses; the Gateway sends the key in the Api-Key header to api.pinecone.io and that host. Query, search, embed and rerank are POSTs, so an app that uses them asks for write. PINECONE_API_KEY: An API key from the project's API keys page in the Pinecone console. PINECONE_HOST: The host of the index the app uses, -.svc..pinecone.io, from describe_index or the console. capabilities: { pinecone }
MAPBOX_ACCESS_TOKEN Mapbox (api.mapbox.com), the key sent as ?access_token=<MAPBOX_ACCESS_TOKEN> on the URL, in place of any the app sent. An access token from Access tokens in the Mapbox console, with only the scopes the app needs; the Gateway adds it to every call as the access_token parameter. An access token with only the scopes the app needs, from Access tokens in the Mapbox console. capabilities: { mapbox }
OPENWEATHER_API_KEY OpenWeather (api.openweathermap.org, pro.openweathermap.org, history.openweathermap.org, tile.openweathermap.org, maps.openweathermap.org), the key sent as ?appid=<OPENWEATHER_API_KEY> on the URL, in place of any the app sent. An API key from the API keys tab of your OpenWeather account; the Gateway adds it to every call as the appid parameter. Paid products (hourly forecast, history, Weather Maps 2.0) need a plan that includes them. An API key from the API keys tab of the OpenWeather account; a new key can take up to two hours to activate. capabilities: { openweather }
VOYAGE_API_KEY Voyage AI (api.voyageai.com), the key sent as Authorization: Bearer <VOYAGE_API_KEY>. A secret key from the Voyage dashboard; the Gateway sends it as a bearer token. Apps POST to /v1/embeddings, /v1/multimodalembeddings, /v1/contextualizedembeddings and /v1/rerank. A secret key from API keys in the Voyage dashboard. capabilities: { voyage }
PERPLEXITY_API_KEY Perplexity (api.perplexity.ai), the key sent as Authorization: Bearer <PERPLEXITY_API_KEY>. An API key from the Perplexity console; the Gateway sends it as a bearer token. Apps POST to /v1/agent for researched answers and /search for result lists, and GET /v1/agent/{id} to poll a background run. An API key from the project's API keys page in the Perplexity console. capabilities: { perplexity }
JINA_API_KEY Jina AI (api.jina.ai, r.jina.ai, s.jina.ai), the key sent as Authorization: Bearer <JINA_API_KEY>. An API key from the Jina API dashboard; the Gateway sends it as a bearer token. Reader is r.jina.ai (POST {"url": ...}), Search is s.jina.ai, and embeddings and rerank are on api.jina.ai. A key starting jina_, from Manage API Key in the Jina API dashboard. capabilities: { jina }
REPLICATE_API_TOKEN Replicate (api.replicate.com), the key sent as Authorization: Bearer <REPLICATE_API_TOKEN>. An API token from Replicate's API tokens page; the Gateway sends it as a bearer token. Apps POST to /v1/models/{owner}/{name}/predictions or /v1/predictions with Prefer: wait (up to 60 seconds), or pass a webhook URL for longer runs. An API token from the API tokens page of the Replicate account. capabilities: { replicate }
HUGGINGFACE_API_TOKEN Hugging Face Inference (router.huggingface.co), the key sent as Authorization: Bearer <HUGGINGFACE_API_TOKEN>. A fine-grained access token from Hugging Face settings with the Inference Providers permission; the Gateway sends it as a bearer token. Apps POST OpenAI-style chat to /v1/chat/completions, or other tasks under //, such as /hf-inference/models/. A fine-grained token with only the Make calls to Inference Providers permission. capabilities: { huggingface }
FIRECRAWL_API_KEY Firecrawl (api.firecrawl.dev), the key sent as Authorization: Bearer <FIRECRAWL_API_KEY>. An API key from Firecrawl's API keys page; the Gateway sends it as a bearer token. Scrape, crawl, map, search and extract are POSTs and job status is a GET; cancelling a crawl or batch (DELETE) needs write. An API key from Firecrawl's API keys page; it starts with fc-. capabilities: { firecrawl }
EXA_API_KEY Exa (api.exa.ai), the key sent as x-api-key: <EXA_API_KEY>. An API key from the Exa dashboard (the free tier gives $10 of credit a month); the Gateway sends it in the x-api-key header. Search, contents, find-similar and answer are POSTs. An API key from the API keys page of the Exa dashboard. capabilities: { exa }
TAVILY_API_KEY Tavily (api.tavily.com), the key sent as Authorization: Bearer <TAVILY_API_KEY>. An API key from app.tavily.com (1,000 free credits a month); the Gateway sends it as a bearer token. Search, extract, crawl, map and research are POSTs; a research result is a GET. An API key from the Tavily dashboard; it starts with tvly-. capabilities: { tavily }
SERPER_API_KEY Serper (google.serper.dev, scrape.serper.dev), the key sent as X-API-KEY: <SERPER_API_KEY>. An API key from serper.dev (2,500 free queries to start); the Gateway sends it in the X-API-KEY header. Every search is a POST to google.serper.dev/ (search, news, images, places, …) with a JSON body such as {"q": "…"}. The API key on serper.dev's API keys page. capabilities: { serper }
BRAVE_SEARCH_API_KEY Brave Search (api.search.brave.com), the key sent as X-Subscription-Token: <BRAVE_SEARCH_API_KEY>. Activate a plan (it includes $5 of free credit a month), then create a key under API Keys; the Gateway sends it in the X-Subscription-Token header. Searches are GETs (POST works too); answers are a POST to /res/v1/chat/completions. A key from the API Keys section of the Brave Search API dashboard, made after activating a plan. capabilities: { brave-search }
APIFY_API_TOKEN Apify (api.apify.com), the key sent as Authorization: Bearer <APIFY_API_TOKEN>. The API token from Apify Console, Settings > API & Integrations; the Gateway sends it as a bearer token. Reading Actors, runs and datasets needs read; starting a run is a POST, so it needs write. The API token under Settings > API & Integrations in Apify Console. capabilities: { apify }
DEEPGRAM_API_KEY Deepgram (api.deepgram.com, api.eu.deepgram.com, api.au.deepgram.com, api.in.deepgram.com), the key sent as Authorization: Token <DEEPGRAM_API_KEY>. An API key from the Deepgram Console; the Gateway sends it as Authorization: Token , and the EU, Australia and India hosts take the same key. Transcription (/v1/listen), speech (/v1/speak) and text analysis (/v1/read) are POSTs. A project API key created in the Deepgram Console. capabilities: { deepgram }
ELEVENLABS_API_KEY ElevenLabs (api.elevenlabs.io), the key sent as xi-api-key: <ELEVENLABS_API_KEY>. An API key from ElevenLabs' API keys page; the Gateway sends it in the xi-api-key header. Speech, transcription and sound generation are POSTs; listing voices and models are GETs. An API key from ElevenLabs' API keys page; it can be scoped to the endpoints the app uses and given a credit cap. capabilities: { elevenlabs }
ASSEMBLYAI_API_KEY AssemblyAI (api.assemblyai.com, api.eu.assemblyai.com), the key sent as Authorization: <ASSEMBLYAI_API_KEY>. The API key from your AssemblyAI dashboard; the Gateway sends it as the whole Authorization header, with no Bearer word. Audio is submitted with a POST to /v2/transcript and the app polls GET /v2/transcript/; deleting a transcript needs write. The API key on your AssemblyAI dashboard. capabilities: { assemblyai }
NEWSAPI_API_KEY NewsAPI (newsapi.org), the key sent as X-Api-Key: <NEWSAPI_API_KEY>. An API key from newsapi.org/register; the Gateway sends it in the X-Api-Key header. The free Developer plan allows 100 requests a day, delays articles by 24 hours and is for development only; production needs a paid plan. The API key NewsAPI shows after you register. capabilities: { newsapi }

Registered providers

A provider the catalog doesn't have can be registered on one Gateway as a row of its providers table (pnpm everyapp provider add <spec>; /docs/agent/providers). Its settings are Gateway settings too, held on the Gateway Worker for every app, but they aren't in SETTABLE_GATEWAY_SETTINGS: the rows route them. Three rules hold for them:

  • The prefix. Every setting a row names starts with its id upper-cased, - as _ (STRIPE_API_KEY for stripe), and is nobody else's: not one of the Gateway's own, not a catalog provider's, not another row's, not a name a registered app lists under secrets. provider add refuses anything else with PROVIDER_SECRET_TAKEN, and the Gateway skips such a row on every read.
  • The routing. pnpm everyapp secret set STRIPE_API_KEY from any directory: the name isn't a catalog setting, so the command connects first, finds the row that holds it and sets it on the Gateway Worker. Before it asks for the value it prints STRIPE_API_KEY is Stripe's, registered by <who> on <date>; the key is sent to api.stripe.com.
  • The binding. Once set, the row records what the key was set for (its hosts and auth); the Gateway reads the key only while the row still says the same. A row added and not yet given a key, or replaced (provider add --replace) since, reads as not set everywhere ("Needs setup", ! not set) and its calls answer integration_not_configured until secret set runs again. provider remove leaves the setting on the Gateway: secret unset removes it.

OAuth clients

One client per provider serves every organization on the Gateway, and two flows run on it (OAuth providers). The app-driven flow: the app lists capabilities: { <id>: { access: "write" } }, builds the authorize URL from integrationStatus()'s clientId, exchanges codes and refreshes tokens through integrationFetch() on the network's token endpoints (the Gateway puts the client on the request), keeps the user tokens and calls the network's API itself. The Gateway-owned flow (google-workspace, microsoft-365, slack, discord, and every OAuth provider added from a preset or a spec): the person connects once at /connect/<id> and the Gateway holds the tokens sealed; the app lists the provider with access: "read" (or write) and its scopes, and integrationFetch() reaches the provider's API hosts with the connected person's token put on by the Gateway. For that flow the client may also be pasted in Manage workspace → Providers instead of set here: the pasted copy is sealed in the oauth_clients table and is read only while the Worker setting of the same name is unset. A client with scope sets also names the sets the app uses, capabilities: { "google-workspace": { access: "write", scopes: ["gmail.read", "calendar.read"], why } } (google-workspace: identity, gmail.read, calendar.read, search-console.read, analytics.read; microsoft-365: identity, mail.read, calendar.read, all read-only; slack: channels.read, users.read, users.email, messages.write, channels.join, channels.manage, files.write, user-channels.manage), and the operator allows on the client's console the provider scopes behind the sets of every app they approve. The client secret never reaches an app. Both names of a pair are required. Step by step for the three with a skill: setup/google.md (the GOOGLE_WORKSPACE_* pair), setup/microsoft-365.md, setup/slack.md.

Settings Manifest entry
LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET capabilities: { linkedin }
FACEBOOK_APP_ID, FACEBOOK_APP_SECRET capabilities: { facebook }
INSTAGRAM_APP_ID, INSTAGRAM_APP_SECRET capabilities: { instagram }
THREADS_APP_ID, THREADS_APP_SECRET capabilities: { threads }
YOUTUBE_CLIENT_ID, YOUTUBE_CLIENT_SECRET capabilities: { youtube }
TIKTOK_CLIENT_ID, TIKTOK_CLIENT_SECRET capabilities: { tiktok }
TIKTOK_BUSINESS_CLIENT_ID, TIKTOK_BUSINESS_CLIENT_SECRET capabilities: { "tiktok-business" }
REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET capabilities: { reddit }
PINTEREST_CLIENT_ID, PINTEREST_CLIENT_SECRET capabilities: { pinterest }
DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET capabilities: { discord }
SLACK_CLIENT_ID, SLACK_CLIENT_SECRET capabilities: { slack: { scopes } }
MASTODON_CLIENT_ID, MASTODON_CLIENT_SECRET, and MASTODON_URL (optional: the https origin of the instance the client is registered on; https://mastodon.social when unset) capabilities: { mastodon }
KICK_CLIENT_ID, KICK_CLIENT_SECRET capabilities: { kick }
TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET capabilities: { twitch }
TUMBLR_CLIENT_ID, TUMBLR_CLIENT_SECRET capabilities: { tumblr }
DRIBBBLE_CLIENT_ID, DRIBBBLE_CLIENT_SECRET capabilities: { dribbble }
GOOGLE_WORKSPACE_CLIENT_ID, GOOGLE_WORKSPACE_CLIENT_SECRET (Gmail, Calendar, Search Console and Analytics, read-only) capabilities: { "google-workspace": { scopes } }
MICROSOFT_365_CLIENT_ID, MICROSOFT_365_CLIENT_SECRET (Outlook through Microsoft Graph, read-only; the common multi-tenant token endpoint only) capabilities: { "microsoft-365": { scopes } }

The developer consoles, redirect URIs, token endpoints and scopes of each client are in /docs/agent/repo/capabilities, generated from packages/sdk/src/internal/capabilities.ts. The Gateway-owned flow's redirect URI is https://<gateway>/connect/<id>/callback for every provider. An OAuth provider added as a row (pnpm everyapp provider add --oauth github-oauth) holds its client in the row's own family, <ID>_CLIENT_ID and <ID>_CLIENT_SECRET (GITHUB_OAUTH_CLIENT_ID, GITHUB_OAUTH_CLIENT_SECRET), routed by the rows as any registered provider's setting is and bound to the row's endpoints (oauth.client-bound-to-endpoints). X's OAuth 1.0a is not a capability: it signs every call with the user's token secret, so an app keeps X_API_KEY and X_API_SECRET as its own secrets; X's OAuth 2 is the x-oauth preset.

Email

The email service capability (sendEmail()), and the Gateway's own invitation and password-reset mail. The sender needs an address and a display name (parseEmailSender in packages/sdk/src/internal/email.ts): EMAIL_FROM is required; the name comes from EMAIL_FROM_NAME or, when that is unset, from EMAIL_FROM written as Name <address> (surrounding quotes are dropped), so EMAIL_FROM_NAME is required unless EMAIL_FROM carries the name. The Gateway can send once that sender parses and a provider is selectable: isEmailConfigured() in apps/every-app-gateway/src/server/email/sendEmail.ts is configuredSender() (the parse above) and selectEmailProvider() (chooseEmailProvider, below) both answering. Its own mail otherwise fails EMAIL_SENDER_NOT_CONFIGURED: Email sender is not configured. Set EMAIL_FROM and EMAIL_FROM_NAME, or use EMAIL_FROM="Name <email@example.com>". when EMAIL_FROM is set without a name, else Email sender is not configured. Set EMAIL_FROM and EMAIL_FROM_NAME. The capability registry lists EMAIL_FROM and EMAIL_FROM_NAME as the capability's settings and the four provider settings as optionalSettings (packages/sdk/src/internal/capabilities.ts), so the Gateway's setup dialog and secret list name both; secret list reads names, not values, so it calls EMAIL_FROM_NAME unset even when EMAIL_FROM carries the name. Whether the Gateway can send is isEmailConfigured(), above. Step by step: setup/email.md.

Setting Configures
EMAIL_FROM The sender address a message without from goes out as: an address, or Name <address> with the display name in it. Required. The sender is null unless both an address and a name are set.
EMAIL_FROM_NAME The sender's display name; when set it wins over a name embedded in EMAIL_FROM. Required unless EMAIL_FROM carries the name.
EMAIL_REST_API_TOKEN Cloudflare Email Sending's REST API, with CLOUDFLARE_ACCOUNT_ID, instead of the Gateway's EMAIL binding (a Gateway outside Workers, local dev).
RESEND_API_KEY Resend as the provider.
EMAIL_PROVIDER cloudflare or resend (any case): which provider sends when both are set up; it must be set up. Unset: the first provider in cloudflare, resend order the operator gave settings to, else the EMAIL binding (chooseEmailProvider). An unknown value leaves the Gateway unable to send.

An app's own senders and inbound addresses are not Gateway settings: pnpm everyapp email set --sender … --inbound … in the app's directory, after its first deploy (/docs/agent/email).

MCP

Switches of the Gateway's /mcp and its OAuth authorization server (apps/every-app-gateway/src/server/oauth-flag.ts, mcp-auth/, toolbus/gateway-mcp.ts). No app lists anything for these.

Setting Configures
MCP_OAUTH_ENABLED The OAuth authorization server for MCP clients: on when unset or true (1, on, yes); off with false (0, off, no). Off, /oauth/* and the discovery documents 404 and /mcp takes personal access tokens only. Any other value is off, with a warning mcp.oauth.flag_unrecognized.
MCP_DCR_ENABLED Dynamic client registration (/oauth/register), the same values. Off, only clients with a Client ID Metadata Document connect; clients registered before keep working.
MCP_ALLOWED_ORIGINS Browser origins, besides the Gateway's own, allowed to call /mcp (comma or space separated http or https URLs, e.g. http://localhost:6274 for MCP Inspector). Empty by default.
MCP_AUDIT_RETENTION_DAYS Days of mcp_tool_calls rows the hourly cron keeps: an integer of 1 or more, capped at 3650; unset, a non-integer or a value below 1 is the default, 90 (retentionDays in apps/every-app-gateway/src/server/mcp-auth/maintenance.ts).

MCP apps

An MCP app's key or credential is a Gateway setting like any other (/docs/agent/mcp). The built-in DataForSEO app reads DATAFORSEO_API_KEY (API-key capabilities, above; step by step setup/dataforseo.md). A remote MCP app, any MCP server added with pnpm everyapp mcp add <url>, reads one setting its registry row names, in a family of its own:

Setting Configures
<ID>_MCP_TOKEN The credential the Gateway sends to the remote MCP server with that id, and only to the origin it is pinned to: the bearer token, the header's value, or user:password for basic auth, as the row's remote.auth says. Any name matching REMOTE_MCP_SECRET_PATTERN (/^[A-Z][A-Z0-9_]*_MCP_TOKEN$/, isRemoteMcpSecretName in packages/sdk/src/internal/gatewaySettings.ts) is one.
  • The family. mcp add names the credential <ID>_MCP_TOKEN, the app id upper-cased with - as _ (remoteMcpSecretName); --secret <NAME> picks another name ending in _MCP_TOKEN, and nothing else. Because the family is a fixed rule, secret set sends such a name to the Gateway without reading the registry, an app's own secret can't take one (below), and an owner or admin adding a server from Admin → Apps can't point a key the Gateway already holds (OPENAI_API_KEY) at their own URL. The settings model reads the row's credential as one required Gateway setting (kind remote-mcp, setup guide setup/remote-mcp.md); a server added with --auth none needs nothing.
  • Setting it. pnpm everyapp secret set TREG_MCP_TOKEN, or mcp add itself: at a terminal it asks Set TREG_MCP_TOKEN now? and reads the value with the hidden prompt; mcp add --from-env VAR reads it from the environment. Never from the Gateway's UI, never over MCP. The value goes where every Gateway setting goes, and nowhere else: never to an AI client, an audit row, a log line, an error or a tool result.
  • The pin. A credential is pinned to one origin. After setting the value, secret set writes (name, origin, bound_by, bound_at) to remote_mcp_credentials in the Gateway's D1 (migration 0011), the origin being that of the live MCP app that declares the name; setting it again re-pins it to that app's origin. The Gateway sends a credential only to the origin it is pinned to, so an owner or admin who re-points a row, or removes one and adds another under the same id, can't move it: such a row needs setup until the operator runs secret set again. A credential set some other way (the Cloudflare dashboard) has no pin and is sent nowhere: every row naming it reads as needing setup (mcp.remote.credential_unpinned) until the operator runs secret set, which pins it; the Gateway never pins on first use (since 2026-10-25; before, the first row to use a name took it). The pin failing to write after the value is set is said (progress secrets.pin), not raised: the value is live but unpinned, so run secret set again.
  • Warnings. REMOTE_MCP_SECRET_UNDECLARED: no live MCP app declares the name; it keeps an earlier pin, if it had one, else the Gateway pins it to the first app that uses it. REMOTE_MCP_SECRET_AMBIGUOUS: live MCP apps at more than one origin declare it (only a row edited by hand can: mcp add refuses a second origin with REMOTE_MCP_SECRET_TAKEN), so it is pinned to none and sent to none until one is removed. (/docs/agent/errors)
  • Unset. pnpm everyapp secret unset TREG_MCP_TOKEN removes the value and its pin. mcp remove removes the app and leaves both.
  • Listing. secret list names each remote credential a live MCP app declares and doesn't have yet among the Gateway's unset names, lists each MCP app that needs setup with its secret set, and carries every credential in data.gateway.mcpCredentials. pnpm everyapp mcp show <id> says whether the credential is set and which origin it is pinned to.

Cloudflare Access and sign-in

Read by apps/every-app-gateway/src/auth/cloudflare-access/config.ts on a self-hosted Gateway only; a hosted Gateway ignores them and EMAIL_PASSWORD_SIGN_IN (its platformPolicy has no Access sign-in; the log says so once, apps/every-app-gateway/src/auth/cloudflare-access/index.ts). Operator-only; see /docs/agent/access and docs/runbooks/cloudflare-access.md. Step by step: setup/access.md.

Three of them the operator does not set by hand: everyapp access on (packages/cli/src/commands/access/index.ts) writes ACCESS_SETTINGS from packages/cli/src/lib/access/settings.ts, CLOUDFLARE_ACCESS_TEAM_DOMAIN, CLOUDFLARE_ACCESS_APPS_AUD and CLOUDFLARE_ACCESS_AUD, in one new version of the Gateway Worker after it has created or updated the two Access applications, through applyGatewaySettings in packages/cli/src/commands/secret/index.ts: the same gateway.secrets lease, replaceable-version check, cf workers secrets bulk call and version record as secret set. Values can't be read back from a Worker, so it records what it wrote (team domain and both AUD tags) in the Gateway's D1, table _everyapp_access (packages/cli/src/lib/access/record.ts), and writes the three again only when one is missing on the Worker, when it created an application, when there is no record or the record doesn't name the current team and applications (an application recreated in the dashboard has a new AUD tag, and the old one locks everyone out), or with --write-settings; otherwise it reports Gateway settings kept: they name these applications already (--write-settings writes them again). everyapp access off removes whichever of the three the Worker holds, in one version, so passwords sign people in again at once, then clears the record. everyapp status reports Access as on when all three are on the Worker (data.access: { on, teamDomain }, accessStateOf in packages/cli/src/lib/access/settings.ts). On a hosted Gateway access on fails ACCESS_HOSTED (This Gateway can't be put behind Cloudflare Access). The rest of the table stays secret set: access on ends by naming pnpm everyapp secret set CLOUDFLARE_ACCESS_AUTO_JOIN_DOMAINS (when run with --email-domain) and pnpm everyapp secret set EMAIL_PASSWORD_SIGN_IN (false, once everyone has signed in through Access) as next steps.

Setting Configures
CLOUDFLARE_ACCESS_OIDC_ISSUER OIDC sign-in through an Access for SaaS application: https://<team>.cloudflareaccess.com/cdn-cgi/access/sso/oidc/<client id>. All three OIDC values are needed, or OIDC sign-in is off.
CLOUDFLARE_ACCESS_OIDC_CLIENT_ID The SaaS application's client id (the issuer must end in it).
CLOUDFLARE_ACCESS_OIDC_CLIENT_SECRET Its client secret.
CLOUDFLARE_ACCESS_OIDC_LABEL The sign-in button's label: Continue with <label>.
CLOUDFLARE_ACCESS_TEAM_DOMAIN The outer ring (Access in front of the Gateway's hostnames): <team>.cloudflareaccess.com. Needs CLOUDFLARE_ACCESS_AUD too, or the ring is off.
CLOUDFLARE_ACCESS_AUD The AUD tag(s) of the Access application on the Gateway's own host, the only one whose assertions sign people in (comma separated).
CLOUDFLARE_ACCESS_APPS_AUD The AUD tag(s) of a separate Access application on the apps' hosts, which only vouches for sessions. Does nothing without the outer ring.
CLOUDFLARE_ACCESS_AUTO_JOIN_DOMAINS Email domains (comma separated) whose people join as members without an invitation; an entry that isn't an email domain is skipped with a warning.
CLOUDFLARE_ACCESS_OUTER_RING_AUTO_JOIN everyone: anyone the outer ring admits joins. Any other value is off; does nothing without the outer ring.
EMAIL_PASSWORD_SIGN_IN false (or 0, off, no) turns password sign-in off, only while an Access sign-in or a sign-in provider (below) is configured and an owner has connected one (otherwise ignored with a warning, so nobody is locked out); unset, true, 1, on or yes keep it on; any other value keeps it on, with a warning (Use "false".). Trimmed, case-insensitive (readPasswordSignIn).
HOSTED_OPEN_SIGNUP Hosted Gateway only: open sign-up when unset or true; false (or any other value) keeps it invite-only. Ignored when self-hosted, which is always invite-only (apps/every-app-gateway/src/server/platform-policy.ts).

Sign-in providers

"Continue with Google" (Microsoft, GitHub, Apple, Cloudflare, or any OpenID Connect issuer) beside passwords, on self-hosted and hosted Gateways alike. The catalog is packages/sdk/src/internal/signInProviders.ts (SIGN_IN_PROVIDERS); the Gateway reads the settings in apps/every-app-gateway/src/auth/sign-in-providers/config.ts. The person's page is Sign in with Google and others; every command, flag, code and rule is Sign-in providers, and the operator's runbook docs/runbooks/sign-in-providers.md. Step by step: setup/signin-google.md, setup/signin-microsoft.md, setup/signin-github.md, setup/signin-apple.md, setup/signin-cloudflare.md, setup/signin-oidc.md.

Every name is <ID>_SIGNIN_<FIELD>, FIELD one of CLIENT_ID, CLIENT_SECRET, ISSUER, LABEL or TENANT (SIGN_IN_SETTING_PATTERN, isSignInSettingName). The _SIGNIN_ infix keeps the family apart from the OAuth clients the Gateway holds for apps (GOOGLE_WORKSPACE_CLIENT_ID, above), the API-key providers' <ID>_API_KEY and the MCP apps' <ID>_MCP_TOKEN: a registered provider row can't take a name in the family (PROVIDER_SECRET_TAKEN, rowSecretProblem), nor can an app's manifest (secrets refuses it: "is a sign-in provider's setting name"). The values are Worker secrets, set with pnpm everyapp signin add <id> (which asks for each one, hidden for the client secret, and prints the redirect URL to register with the provider; unattended --client-id, --client-secret-from-env <VAR>, --issuer and --label for oidc, --tenant for microsoft; the whole family in one Gateway version, a setting set already and not given kept) or pnpm everyapp secret set <NAME> (which checks the issuer, the tenant and the label as the Gateway reads them, and refuses a name of the family no provider reads); pnpm everyapp signin list and signin show <id> say what is set, signin remove <id> unsets the family; never from the Gateway's UI, never over MCP (a stolen session or token can't add a way in). A provider is on only when every required setting is set; with some set and some missing it is off, the log says what is missing (signin.config_problem, once per isolate) and Manage workspace → Sign-in shows it as needing setup. The redirect URL to register is https://<gateway>/api/auth/callback/<id> (signInCallbackUrl).

Setting Configures
GOOGLE_SIGNIN_CLIENT_ID The Google Cloud OAuth client's Client ID (Web application; ends in .apps.googleusercontent.com). With the secret, "Continue with Google".
GOOGLE_SIGNIN_CLIENT_SECRET Its Client secret.
MICROSOFT_SIGNIN_CLIENT_ID The Entra app registration's Application (client) ID. With the secret, "Continue with Microsoft".
MICROSOFT_SIGNIN_CLIENT_SECRET A client secret from the registration's Certificates & secrets (expires; 24 months at most).
MICROSOFT_SIGNIN_TENANT Optional: the Directory (tenant) ID (a GUID) that pins sign-in to the operator's own directory, whose addresses are then trusted; or organizations, consumers or common (the default when unset: any Microsoft account), where an address counts as verified only with the token's xms_edov: true, email_verified: true, a matching verified_primary_email or a personal account. Anything else turns Microsoft off with a warning.
GITHUB_SIGNIN_CLIENT_ID The GitHub OAuth App's Client ID. With the secret, "Continue with GitHub"; the email is the primary address GitHub marks verified.
GITHUB_SIGNIN_CLIENT_SECRET A client secret generated on the OAuth App.
APPLE_SIGNIN_CLIENT_ID The Services ID's identifier. With the secret, "Continue with Apple".
APPLE_SIGNIN_CLIENT_SECRET The client secret JWT generated from the Sign in with Apple key (ES256; valid six months at most, then generate it again).
CLOUDFLARE_SIGNIN_CLIENT_ID A Cloudflare OAuth client's Client ID (Manage Account → OAuth clients; private, so only the account's members can authorize it). With the secret, "Continue with Cloudflare"; the email is the account's sign-in address from the API's /user, trusted because the client is private (Cloudflare reports no verified flag). Keep it private: making it public is permanent and lets any Cloudflare user in.
CLOUDFLARE_SIGNIN_CLIENT_SECRET Its Client secret.
OIDC_SIGNIN_ISSUER A generic OpenID Connect issuer's URL (https://…, no query or fragment; a loopback http:// only in vite dev), which serves /.well-known/openid-configuration; the document must name the same issuer. Trailing slashes are dropped. With the client id and secret, "Continue with
OIDC_SIGNIN_CLIENT_ID The client ID registered with that issuer.
OIDC_SIGNIN_CLIENT_SECRET Its client secret.
OIDC_SIGNIN_LABEL Optional: the button text after "Continue with" (40 characters at most; default SSO).

Deployers and the hosted tier

Setting Configures
CF_DEPLOY_TOKEN A Cloudflare API token for CLOUDFLARE_ACCOUNT_ID the Gateway deploys apps with (apps/every-app-gateway/src/server/deploy/cfRestDeployer.ts). Self-hosted: browser app installs (Admin → Apps → Install app), which need Workers Scripts Edit, D1 Edit and DNS Read for the domain's zone (docs/runbooks/browser-app-install.md); interactive pnpm everyapp setup offers to set it when the Gateway has none. Hosted: the third-party deploy API, which needs Workers for Platforms and D1 edit. Whoever reads the Gateway's secrets can deploy with it.
BROWSER_APP_INSTALLS Self-hosted browser installs are off until this is exactly on beside CF_DEPLOY_TOKEN, CLOUDFLARE_ACCOUNT_ID and APP_BASE_DOMAIN (setup writes it with the token when the person opts in); off, unset or anything else keeps them off; on self-host they are the install organization's owners' and admins', from a session signed in within 10 minutes (apps/every-app-gateway/src/server/deploy/browser-install.ts, docs/runbooks/browser-app-install.md).
DISPATCH_NAMESPACE Hosted: the Workers for Platforms namespace third-party Workers go into; the Gateway's DISPATCHER binding must name the same.
THIRD_PARTY_DOMAIN Hosted: the registrable domain third-party apps are named on (<app>-<orgKey>.<domain>), each host with its own session. Unset, on the Gateway's own site, or on a self-hosted Gateway: ignored, so such apps get no hostname. See docs/runbooks/hosted-install.md.

Events out

The event bus's one switch (/docs/agent/events); no app lists anything for it.

Setting Configures
EVENTS_ALLOW_PRIVATE_TARGETS Self-hosted only: exactly true lets an event webhook subscription target http: URLs, IP literals and private names (localhost, *.local, *.internal, *.lan, *.corp, a bare host name, …: EVENT_PRIVATE_NAME_SUFFIXES in packages/sdk/src/internal/events.ts), for an operator wiring an internal n8n; false or unset keeps the rules (https:, a public hostname). The Gateway's own host and its apps' hosts are refused whatever it says (apps/every-app-gateway/src/server/events/target.ts). An RFC 1918 or loopback address is not routable from Cloudflare's edge in any case, so the setting buys http: and internal names that resolve publicly, nothing more. Checked again before every delivery attempt, so unsetting it stops deliveries to such URLs.

Install-owned settings: never set by hand

INSTALL_OWNED_GATEWAY_SETTINGS: GATEWAY_URL, APP_BASE_DOMAIN, BETTER_AUTH_SECRET, JWT_PRIVATE_KEY, JWT_PUBLIC_KEY, GATEWAY_DEPLOYMENT_MODE, CLOUDFLARE_ACCOUNT_ID, THIRD_PARTY_DEPLOYS. secret set refuses them outside an app's directory (SECRET_NOT_SETTABLE); in an app's directory they are app names like any other: SECRET_NOT_DECLARED unless the manifest lists them, in which case they go to the app (notRoutable in packages/cli/src/commands/secret/index.ts). secret list shows the ones present on the Gateway as setup.

  • The Gateway's first upload generates and sends GATEWAY_URL, BETTER_AUTH_SECRET (32 random bytes, base64), JWT_PRIVATE_KEY and JWT_PUBLIC_KEY (RS256, 2048-bit, PKCS#8 and SPKI PEM), GATEWAY_DEPLOYMENT_MODE (self_hosted on a Worker's first upload; hosted is the only other value apps/every-app-gateway/src/server/platform-policy.ts accepts, the hosted Gateway this page refers to, and no command sets it any more; anything else, unset included, is invalid and turns every capability off; a mode the install row already records is kept, so it is fixed for the Gateway's life: packages/cli/src/lib/gateway/apply.ts) and CLOUDFLARE_ACCOUNT_ID with cf deploy --secrets-file (packages/cli/src/lib/gateway/secretsFile.ts). APP_BASE_DOMAIN is a text binding the deploy patches into the Gateway's Build Output (packages/cli/src/lib/gateway/patchBuildOutput.ts).
  • THIRD_PARTY_DEPLOYS is the hosted launch gate: exactly on opens the third-party deploy API. The CLI never sets it: set it by hand on the every-app-gateway Worker's secrets once the launch checks pass, and delete it to close the API again (deployed apps keep running; nobody can deploy) (docs/runbooks/hosted-install.md, /docs/agent/repo/runbooks/hosted-install).
  • An app's own Worker may hold a secret of the same name (OpenSEO's BETTER_AUTH_SECRET): list it under the app's secrets and set it from the app's directory; the two never meet.
  • Rotating the identity pair is docs/runbooks/key-rotation.md (/docs/agent/repo/runbooks/key-rotation): generate the pair with node apps/every-app-gateway/regenerate-secrets.js (a PKCS#8 private key and an SPKI public key, RS256, 2048-bit; an OpenSSH public key or a PKCS#1 BEGIN RSA PRIVATE KEY private key deploys without error and fails at runtime), replace JWT_PRIVATE_KEY and JWT_PUBLIC_KEY together in the Cloudflare dashboard (never one half), write the new public key into the install row (_everyapp_install.jwt_public_key, where app deploys read it), run pnpm everyapp setup --redeploy in a terminal, then pnpm everyapp deploy from every app's directory, since each app carries the public key in EVERYAPP_IDENTITY_PUBLIC_KEYS. Until every app is redeployed, apps with the old key answer 401 to new identities: the SDK tries each key in EVERYAPP_IDENTITY_PUBLIC_KEYS and throws IdentityError (status 401, identity verification failed: …, packages/sdk/src/internal/identity.ts), which the app's handler answers as { "error": "unauthenticated" } (packages/sdk/src/server/everyApp.ts). A deploy that stops with IDENTITY_KEY_UNKNOWN is reading an install row with no public key (a Gateway first deployed by an older CLI): do the install-row step with the Gateway's current JWT_PUBLIC_KEY. Every other key (a provider's, a client's, an app's own) rotates with pnpm everyapp secret set NAME again: one new Worker version, live at once.

Dev-only settings

DEV_ONLY_GATEWAY_SETTINGS: MCP_ACCESS_TOKEN_TTL and DATAFORSEO_SANDBOX. Set in the Gateway's own .dev.vars, never deployed, honored only when GATEWAY_URL starts with http://; secret set refuses them (SECRET_NOT_SETTABLE). MCP_ACCESS_TOKEN_TTL: shorter OAuth access tokens in seconds (clamped to 60 through 3600, the normal lifetime; apps/every-app-gateway/src/server/mcp-auth/config.ts). DATAFORSEO_SANDBOX: true sends the DataForSEO MCP app's tool calls (/docs/agent/mcp#mcp-apps "MCP apps") to sandbox.dataforseo.com, which answers like production with made-up data at no charge, still with the DATAFORSEO_API_KEY from the same file (apps/every-app-gateway/src/server/provider-apps/dataforseo.ts).

Reserved names an app secret cannot take

Checked without regard to case, in the CLI (assertAppSecretName, packages/cli/src/lib/app/secrets.ts), the manifest validator and everyAppWorker():

Reserved Why
GATEWAY_URL The deploy sets it on every Worker.
CF_VERSION_METADATA The version-metadata binding every Worker gets.
The prefix EVERYAPP_ The identity settings and dev flags (EVERYAPP_APP_ID, EVERYAPP_DEV, EVERYAPP_IDENTITY_PUBLIC_KEYS, …).
The prefix EVERY_APP_ The gateway binding and organization (EVERY_APP_GATEWAY, EVERY_APP_ORG_ID).
The prefix APP__ The platform's own namespace.

Nor can an app's secret end in _MCP_TOKEN, in any case: the family belongs to the Gateway's remote MCP credentials ("MCP apps" above). The strict manifest validator refuses it (<NAME> is a remote MCP app's credential name (every name ending in _MCP_TOKEN is the Gateway's, set with `everyapp secret set`); give the app's secret another name, packages/perimeter/src/manifest/manifest.ts), and secret set sends a name in the family to the Gateway from any directory, so it never reaches an app's Worker.

The SDK keeps the same rule as a regex in packages/sdk/src/cloudflare/worker.ts (^(GATEWAY_URL|CF_VERSION_METADATA|EVERYAPP_\w*|EVERY_APP_\w*|APP__\w*)$, case-insensitive): a manifest listing one fails the Worker definition with secrets: NAME is a name the deploy sets or a binding it builds.

App secrets

Declaring them

In everyapp.config.ts:

export default {
  id: "todo",
  secrets: ["GITHUB_TOKEN", "STRIPE_KEY"],
} as const;

A name may also be an object: { name: "GITHUB_TOKEN", why, docs, optional: true } or anyOf: "<group>", and setup: "setup/<name>.md" for its guide in the everyapp skill. A bare name is required: the app shows Needs setup and doesn't open until it is set (pnpm everyapp configure <app> asks for it); optional: true lets it open without; one member of an any-of group is enough. Rules and messages: /docs/agent/manifest.

AppSecretsSchemaStrict in packages/perimeter/src/manifest/manifest.ts checks the list when the CLI parses the manifest (validateManifestStrict; errors read Invalid everyapp.config: secrets.<index>: …):

Refusal When
binding names must be JavaScript identifiers Not ^[A-Za-z_][A-Za-z0-9_]*$. The Gateway's registration parse (validateManifest, tolerant of unknown keys) applies the same pattern as a secret's name is an identifier and also caps a name at 64 characters and the list at 100.
duplicate secret NAME Listed twice.
NAME is set by everyapp itself (GATEWAY_URL, CF_VERSION_METADATA and the EVERYAPP_*, EVERY_APP_* and APP__* names) A reserved name, in any case.
NAME is a Gateway setting, which `everyapp secret set NAME` sets on the Gateway for every app; list `<id>` under providers instead (or under capabilities instead) The upper-cased name is in SETTABLE_GATEWAY_SETTINGS and an AI provider or capability reads it (gatewaySettingOwner): openai_api_key names openai, DATAFORSEO_API_KEY names dataforseo.
NAME is a Gateway setting, which `everyapp secret set NAME` sets on the Gateway for every app; give the app's secret another name A settable name no provider or capability owns (MCP_OAUTH_ENABLED, CF_DEPLOY_TOKEN, …).
Invalid everyapp.config: secrets: NAME is one of the app's bindings; pick another name for the secret The name is a D1, KV, R2, Durable Object, Workflow or Hyperdrive binding, a helper Worker's binding, DATABASE_PROVIDER (when database is set) or ASSETS (bindingNamesOf).

Install-owned Gateway names are not refused here: an app may list BETTER_AUTH_SECRET as its own.

Setting and reading them

  1. Deploy the app once (pnpm everyapp deploy): the production build declares no app secret, and the upload carries only GATEWAY_URL and EVERY_APP_ORG_ID.
  2. pnpm everyapp configure todo asks for each, from anywhere (pnpm everyapp configure todo --from-env --json for an agent). One name at a time, from the app's directory: pnpm everyapp secret set GITHUB_TOKEN (a person at the prompt) or pnpm everyapp secret set GITHUB_TOKEN --from-env GITHUB_TOKEN --json (an agent). The value lands on the app's Worker and on every helper Worker in one new version each; later deploys keep it: the production build declares no app secret (packages/sdk/src/cloudflare/worker.ts, the secrets field), and cf deploy (the CLI's pinned cf, 1.0.0-beta.14) uploads every version with keepSecrets on, which puts secret_text and secret_key in the upload's keep_bindings, so a secret absent from the Build Output and the --secrets-file stays on the Worker.
  3. In the Worker, env.GITHUB_TOKEN is the string. Type it as optional, since it is absent in development until set locally, by adding to the app's src/env.d.ts:
declare namespace Cloudflare {
  interface Env {
    GITHUB_TOKEN?: string;
    STRIPE_KEY?: string;
  }
}

(the shape packages/sdk/src/env.d.ts and apps/every-app-gateway/src/env.d.ts use; cf generates the bindings' own types from cloudflare.config.ts).

  1. pnpm everyapp secret unset GITHUB_TOKEN removes it from every Worker that holds it. Dropping the name from the manifest does not remove the value: unset still accepts the dropped name.

A secret's value is never read back by the CLI: secret list and the Gateway's Admin → Apps (where a shared app's manifest, its secrets names included, is visible to every organization's admins) show names only.

In development

everyapp dev runs the app with cf dev and no Gateway binding. everyAppWorker() (packages/sdk/src/cloudflare/worker.ts, addSecrets) declares as a secret binding each of these names that is set locally, so the Vite plugin passes the value through:

  • every name under the manifest's secrets;
  • the Gateway settings of the manifest's providers and capabilities (DEV_GATEWAY_KEYS: OPENAI_API_KEY, OPENROUTER_API_KEY, WORKERS_AI_API_TOKEN with CLOUDFLARE_ACCOUNT_ID, DATAFORSEO_API_KEY, CONTEXT_API_KEY, each OAuth client's id and secret and MASTODON_URL, email's EMAIL_FROM, EMAIL_FROM_NAME, EMAIL_REST_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, RESEND_API_KEY, EMAIL_PROVIDER), under the same names the Gateway uses;
  • EVERYAPP_EMAIL_SENDERS when the manifest lists email (the senders standing in for everyapp email set --sender).

"Set locally" is read as the Vite plugin reads secrets (localEnv): when .dev.vars.<mode> or .dev.vars exists in the app root, that file alone; otherwise .env, .env.local, .env.<mode>, .env.<mode>.local and the process environment, later ones winning. A name that is not set is not declared, so env.NAME is undefined. everyapp dev never writes .dev.vars; keep it out of git.

The SDK's direct fallback then uses those values in place of the Gateway, only with EVERYAPP_DEV set (which everyAppWorker() sets in development):

  • createGatewayFetch({ env }) (packages/sdk/src/server/gatewayFetch.ts): with no binding and no provider fully set, it throws ConfigurationError: AI gateway unavailable: deploy with the EVERY_APP_GATEWAY binding, or set the provider's key (OPENAI_API_KEY, OPENROUTER_API_KEY or WORKERS_AI_API_TOKEN with CLOUDFLARE_ACCOUNT_ID) in .dev.vars for direct local development. A call to a provider that is not set throws Set OPENAI_API_KEY in .dev.vars to call openai in local development. Otherwise the call goes to the provider's origin with Authorization: Bearer <key>; Workers AI's path is joined under /client/v4/accounts/<id>/ai and its stream normalized as the Gateway does. Only /api/ai/openai, /api/ai/openrouter and /api/ai/workers-ai paths are accepted.
  • integrationFetch({ env, provider }) (packages/sdk/src/server/integrationFetch.ts): with no binding and no local credentials it throws <Label> is unavailable: deploy with the EVERY_APP_GATEWAY binding, or set <SETTINGS> in .dev.vars for local development. (e.g. DataForSEO is unavailable: … set DATAFORSEO_API_KEY …; an OAuth client names its id, secret and host setting). Otherwise the call goes straight to the provider with the developer's key, or to the network's token endpoint with the developer's client injected by the Gateway's own rule; an API host or path outside the capability's is a TypeError. Naming a service (email, members) throws <id> is a gateway service: call its helper from @every-app/sdk/server (sendEmail(), listMembers()), not integrationFetch().
  • integrationStatus({ env, provider }) in dev answers requested: true and configured/ready true when EVERYAPP_DEV and the key are set (an OAuth client: its id and secret; email: EMAIL_FROM; members: EVERYAPP_DEV alone).

Deployed, none of this applies: the binding exists, the fallback is never taken, and provider keys set as an app's own secrets are refused by the manifest validator.

An app secret or a Secrets Store secret

An app secret (secrets, set with everyapp configure <app> or secret set) is a value on the app's own Workers, read as the string env.NAME; it is the default for a key only this app uses. A Cloudflare Secrets Store secret (resources.secretsStore: [{ name: "NAME" }], /docs/agent/manifest, "Secrets Store") lives once in the account's store, where the dashboard manages, rotates and audits it, and the Worker fetches it with await env.NAME.get(): the first deploy creates it as <worker>--<binding> from NAME in .everyapp/secrets.env (the store must exist: SECRETS_STORE_MISSING), and later deploys never rewrite it, so its value changes in the dashboard, not with secret set. One name is one or the other: the manifest refuses a secrets name that is also a binding.

Capabilities, providers or your own secret

You need Manifest SDK call Where the credential lives Access
AI inference (OpenAI, OpenRouter, Workers AI) providers: ["openai"], optional ai: { models, routes } createGatewayFetch({ env, for }) on /api/ai/<provider>/v1/... The Gateway (OPENAI_API_KEY, …) inference POSTs; extra routes only with AI_PROXY_EXTRA_ROUTES
A provider API whose key the Gateway holds (DataForSEO, Context.dev) capabilities: { dataforseo: { access: "read", why } } integrationFetch({ env, provider: "dataforseo", for }) on the provider's real URLs The Gateway (DATAFORSEO_API_KEY) read
Connecting people's accounts on a social network, Gmail or Outlook capabilities: { linkedin: { access: "write" } } integrationStatus() for clientId (and Mastodon's origin); integrationFetch() on the token endpoints The Gateway (LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET); user tokens in the app write
Sending mail capabilities: { email: { access: "write" } } sendEmail({ env, for, message }) The Gateway (EMAIL_FROM with a display name from EMAIL_FROM_NAME or embedded in it, and a provider); senders per installation write
The organization's members capabilities: { members: { access: "read" } } listMembers({ env, for }) None: the Gateway's own directory read
Anything else: an API only this app calls, X's OAuth 1.0a keys, a bot token secrets: ["X_API_KEY"] env.X_API_KEY The app's Workers the app's

A capability entry takes access (a level the capability offers; write on an api_key capability or read on an OAuth client is refused at deploy) and an optional why (at most 200 characters). An unknown id is refused: unknown capability (the gateway offers: dataforseo, context, email, members, linkedin, …).

Attribution. Every brokered call says whom it is for (packages/sdk/src/server/attribution.ts): for: request attributes it to the request's user by the identity the Gateway put on it; for: { background: true, organizationId } to an organization for work no request started (crons, Workflows, queues, an email() handler). An organization's own app may omit for for background work (the call is the app's); a shared app's Worker (EVERYAPP_SHARED) must pass one or the other or the SDK throws AttributionError (code attribution_required) before any call. The organization travels as x-everyapp-org. Every attributed provider and AI call is counted per organization, app, member and provider (usage_daily, usage_monthly) and shown under Manage workspace → Usage, where an owner or admin may cap an app's provider calls per UTC month (org_app_settings.monthly_call_cap), set one default cap for every app without a cap of its own (org_settings.default_monthly_call_cap) and leave one app out of the default (org_app_settings.unlimited); every cap is off by default, and the app's own cap wins over the default. Past the effective cap the Gateway answers 429 app_monthly_cap_exceeded and counts nothing (the message names the cap, the workspace default when that is what binds, and the role that raises it, never a person); the app still opens, and its Usage row and its card say Paused. An org_app_settings row holds exactly one of (monthly_call_cap set, unlimited false) or (monthly_call_cap null, unlimited true), both columns written in one statement; the reader lets a cap win. With a cap set, checkMonthlyCap (apps/every-app-gateway/src/server/app-cap.ts) answers { exceeded, cap, calls, month, source } on every check, and that standing goes to the event bus after the answer (cap.warning at 80%, cap.reached and app.paused_cap at the cap; /docs/agent/events), where the built-in Cap alerts mail owners and admins at 80% and at the cap once the Gateway can send mail (Manage workspace → Events). The Gateway's own services (email, members) are neither counted nor capped.

Status. integrationStatus({ env, provider }) answers { requested, configured, ready } (over the binding, GET /api/integrations/<id>, without calling the provider): requested is whether the deployed manifest lists the capability, configured whether the Gateway holds its settings, ready whether calls would go through. configured is false for an app that doesn't list the capability, so no app learns what the operator holds (api_key: apps/every-app-gateway/src/server/integration-proxy.ts; oauth_client: capabilities/oauthClient.ts; email: capabilities/email.ts). members, which holds nothing, answers configured: true whether or not the app lists it and lets only ready follow requested (capabilities/members.ts). email adds sender, senders and provider (cloudflare or resend); an OAuth client adds clientId and, for Mastodon, origin, only once the Gateway holds the client; a client with scope sets (google-workspace, microsoft-365, slack) adds scopeSets (the manifest's names) and scopes (the provider strings for the authorize URL, which authorizeUrl() builds: /docs/agent/identity). Show "an admin needs to set up X" from ready instead of failing mid-task.

Refusals. A refusal the Gateway makes itself carries its code in the x-everyapp-error header; gatewayRefusal(response) returns { status, code, message } or null for the provider's own answer, and sendEmail()/listMembers() throw GatewayRefusalError with the same fields. The codes an app sees for configuration and for the shape of an AI or token call (apps/every-app-gateway/src/server/ai-proxy.ts, integration-proxy.ts, capabilities/oauthClient.ts):

Code Status When
capability_not_requested 403 The deployed manifest doesn't list the capability with the access level the call needs.
integration_not_configured 503 The Gateway holds no key, client or sender for it (or MASTODON_URL isn't an https origin, or EMAIL_PROVIDER names one not set up); the message names the secret set command.
provider_not_allowed 403 The manifest's providers doesn't list the AI provider (The app manifest does not allow this provider.); checked first on every AI call.
ai_provider_not_supported 404 The path names a provider the Gateway has none of (Provider not supported: the gateway has no AI provider "<id>".). The manifest's providers takes any lowercase slug (ProviderNameSchema in packages/perimeter/src/manifest/manifest.ts; no validator checks the id against the Gateway's providers), and the entrypoint checks provider_not_allowed against that list first, so an app that lists foo and calls /api/ai/foo/... meets it.
ai_provider_not_configured 503 The AI provider's key (Workers AI: token or account) isn't set: Provider is not configured: the gateway needs OPENAI_API_KEY for openai.
ai_request_invalid 400 Invalid proxy request path (the path can't be joined under the provider's base URL), or Expected a JSON object body on a route whose body the Gateway reads.
ai_route_not_allowed 403 A path beyond inference that AI_PROXY_EXTRA_ROUTES or the manifest's ai.routes doesn't allow.
model_not_allowed 403 A model outside ai.models.
ai_upstream_failed 502 The AI provider couldn't be reached (Upstream provider request failed).
integration_path_not_allowed 403 An account-wide provider path, or a call to an OAuth network that isn't one of its token endpoints.
integration_method_not_allowed 405 A method the capability doesn't read with.
integration_agent_not_allowed 403 An agent's identity (chan: "mcp") on an OAuth client or members call.
integration_read_only 403 An agent without mcp:write sending mail.
integration_body_too_large 413 A token request's body over 64 KiB, by content-length or as it streams (A token request's body is at most 64 KiB.; OAUTH_TOKEN_BODY_LIMIT in packages/sdk/src/internal/oauthClient.ts). In dev the fallback throws a TypeError with the same message.
integration_body_invalid 400 The Gateway's client couldn't be put on the token request's body; the message says what the endpoint takes (This token endpoint takes a JSON body., This token endpoint takes a form-encoded body (application/x-www-form-urlencoded)., The body is not a JSON object.).
invalid_integration_route 400 A host outside the provider's, or a path that could leave it.
integration_upstream_failed 502 The provider couldn't be reached.
integration_scopes_undeclared 403 A client with scope sets listed without scopes in the deployed manifest entry.
integration_scope_not_allowed 403 A token request asking the provider for scopes beyond the sets the app declares; build it from integrationStatus().scopes.
integration_scope_unverified 502 The provider issued a token without saying which scopes it carries; nothing was passed on.
integration_scope_exceeded 403 The provider granted scopes the app doesn't declare (Google's include_granted_scopes, a wider earlier consent); nothing was passed on, and the message says how the person reconnects narrowly.
app_monthly_cap_exceeded 429 The organization's monthly cap on this app's provider calls is reached, the call cap or the dollar cap on provider-reported cost, its own or the workspace default (retry-after to the next UTC month; the message says which); an owner or admin raises it under Manage workspace → Usage.

members_list_failed, caller_not_registered and invalid_gateway_route are on /docs/agent/identity; the email_* codes on /docs/agent/email. The full check order per kind, the email message rules and codes, and the OAuth clients' tables are in /docs/agent/repo/capabilities (docs/capabilities.md).

The Postgres secrets file

.everyapp/secrets.env in the app root (packages/cli/src/lib/app/deploySecrets.ts) holds what everyapp deploy itself needs to reach the app's Postgres database, in dotenv syntax. The deploy reads it only when the manifest has migrations.dialect: "postgresql", resources.hyperdrive or resources.secretsStore, adds .everyapp/ to the app's .gitignore when it reads it, and never uploads, prints or records a value from it (output that would echo one is redacted). Unlike the app's own secrets, nothing in it reaches the Worker as a value: it binds the Hyperdrive config, which holds the origin's credentials in Cloudflare, and the Secrets Store secret, which holds its value in the account's store. The file also holds the value each new resources.secretsStore secret is created with, under the binding's own name (STRIPE_KEY=…): read only to create a secret that isn't in the store yet, sent to cf in a private body file, and never rewritten on a later deploy; missing when it is needed: SECRETS_STORE_VALUE_MISSING.

Variable Used for
POSTGRES_DATABASE_URL The database drizzle-kit migrate (or prisma migrate deploy on the prisma engine) runs against before the upload, when migrations.dialect is "postgresql". The tool gets it as POSTGRES_DATABASE_URL in its own environment, never argv (packages/cli/src/lib/migrations/localCli.ts), so drizzle.config.ts or prisma.config.ts (datasource: { url: process.env.POSTGRES_DATABASE_URL }) reads process.env.POSTGRES_DATABASE_URL (packages/sdk/README.md). Missing: POSTGRES_URL_MISSING (<id> needs a Postgres URL to migrate its database; fix: set POSTGRES_DATABASE_URL in .everyapp/secrets.env, then deploy again). A run that fails is MIGRATION_FAILED: drizzle-kit couldn't migrate the Postgres database (fix: check POSTGRES_DATABASE_URL in .everyapp/secrets.env and the migrations; packages/cli/src/lib/migrations/postgres.ts) or prisma migrate deploy couldn't migrate the Postgres database (fix: check POSTGRES_DATABASE_URL in .everyapp/secrets.env, prisma.config.ts (its datasource reads it) and the migrations; prisma.ts), with the first error line (Prisma: its P1001-style code line) of the tool's output, the URL and password replaced, and Nothing was uploaded.; the serving version keeps running. Also the origin of the app's Hyperdrive config when it has exactly one.
<BINDING>_ORIGIN_URL The origin a Hyperdrive config for that binding (resources.hyperdrive[].name) connects to; an app with one config may leave it to POSTGRES_DATABASE_URL (hyperdriveOriginUrl). It is the binding's required setting: missing when the config must be created, the deploy stops with HYPERDRIVE_ORIGIN_MISSING (<binding> needs the database its Hyperdrive config connects to then Creating the config needs its URL, from .everyapp/secrets.env on the machine that deploys; nothing in that file reaches the Worker.; fix: set POSTGRES_DATABASE_URL (or <BINDING>_ORIGIN_URL) in .everyapp/secrets.env to postgres://<user>:<password>@<host>:<port>/<database> for the app's only config, set <BINDING>_ORIGIN_URL in … for one of several), and everyapp status names the same variable and file under the app until its first deploy finishes. A config of that name that no deploy of this app created, and that doesn't connect to this origin, is RESOURCE_NAME_TAKEN: A Hyperdrive config named "<name>" already exists in this Cloudflare account then No deploy of this app created it.; fix: to use it, set <BINDING>_ORIGIN_URL in .everyapp/secrets.env to the URL of the database it connects to; otherwise delete or rename it (the fix names the binding's own variable even for an app with one config). Both in provisionHyperdrive, packages/cli/src/lib/app/resources.ts.

Each value is postgres://user:password@host:5432/database (postgres, postgresql or mysql; the port defaults to 5432 or 3306). A value that isn't a URL, isn't one of those schemes, or lacks a host, database, user or password is DATABASE_URL_INVALID, whose message names the variable and never the value. Every origin is checked before any config is created. A Hyperdrive config not recorded as the app's is adopted only when it connects to the origin in this file, so binding it gives the app no database it couldn't already reach; a recorded one whose host, port, database or user differ is pointed at the origin (packages/cli/src/lib/app/resources.ts). In development, a local Postgres URL goes in .dev.vars or .env as CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_<BINDING>. The person writes this file; everyapp configure sets what the Gateway and the app's Worker hold and doesn't write it. Which databases, which URL (Neon's and Supabase's direct one), the code, and Turso (an app secret, not this file): Postgres.

Hosted third-party apps

An app on the dispatch tier (a third-party app the Gateway's own deploy API or a browser install deploys, apps/every-app-gateway/src/server/deploy/) cannot have secrets of its own yet. Both go through parseManifest in run.ts (routes.ts for the deploy API, install.ts for the browser install), which refuses the manifest (manifestProblems in apps/every-app-gateway/src/server/deploy/metadata.ts) as BUNDLE_REFUSED, 422: The app can't be deployed as a third-party app: then it declares secrets, which third-party apps can't have yet; a Build Output with any secret binding besides GATEWAY_URL and EVERY_APP_ORG_ID is refused the same way (bundleProblems: <name> is a secret; third-party apps can't have secrets of their own yet). manifestTierProblems (packages/perimeter/src/manifest/manifest.ts: secrets: third-party apps can't have secrets of their own yet) is the same rule in pnpm everyapp deploy (packages/cli/src/commands/deploy/deployApp.ts), TIER_POLICY (<id> can't be deployed as configured then each problem; fix: change everyapp.config.ts, then deploy again), for an app the registry already records on the dispatch tier. Such an app reaches keys only through providers and capabilities. The deploy API's bearer is a deploy key: a personal access token with the deploy scope and nothing else (* doesn't count), of a live owner or admin of the organization it is bound to, checked on every request and minted in the Gateway's Admin → Tokens as Deploy key (apps/every-app-gateway/src/server/deploy/auth.ts; DEPLOY_SCOPE in apps/every-app-gateway/src/server/user-pat-scopes.ts). The Gateway keeps its hash (hashUserPat, apps/every-app-gateway/src/server/services/UserPatService.ts) and the caller keeps the value, so it is in none of the three places above and never a secret on any Worker.

Security notes

From docs/security-model.md (/docs/agent/repo/security-model):

  • Secret values reach cf only in private temp files (0600, removed afterwards) or the environment, never argv or output; .everyapp/secrets.env values never reach a Worker.
  • secret set|unset change the live Worker without a deploy; they take the Cloudflare login's authority and do not yet check the platform-admin rule of the hosted Gateway.
  • The Gateway's logs redact cookie, authorization, set-cookie and any token, secret, password or code field; capability logs name hosts, paths and status, never bodies or keys.
  • Cloudflare Access settings are Worker secrets only, never the admin UI (everyapp access on|off writes them as Worker secrets too). CF_DEPLOY_TOKEN on a hosted Gateway lets whoever reads the Gateway's secrets deploy into the namespace.

The boundaries these rules follow (which Worker holds which key, what reaches an app's Worker, what only the Gateway brokers) are in docs/threat-model.md (/docs/agent/repo/threat-model).

Sources

  • packages/sdk/src/internal/gatewaySettings.ts
  • packages/sdk/src/internal/remoteMcp.ts
  • packages/sdk/src/internal/appSettings.ts
  • packages/sdk/src/internal/aiProviders.ts
  • packages/sdk/src/internal/capabilities.ts
  • packages/sdk/src/internal/email.ts
  • packages/sdk/src/internal/identity.ts
  • packages/sdk/src/internal/protocol.ts
  • packages/sdk/src/internal/oauthClient.ts
  • packages/sdk/README.md
  • packages/sdk/src/cloudflare/worker.ts
  • packages/sdk/src/server/gatewayFetch.ts
  • packages/sdk/src/server/integrationFetch.ts
  • packages/sdk/src/server/attribution.ts
  • packages/sdk/src/server/everyApp.ts
  • packages/sdk/src/env.d.ts
  • packages/cli/src/commands/configure/command.ts
  • packages/cli/src/commands/configure/index.ts
  • packages/cli/src/commands/secret/command.ts
  • packages/cli/src/commands/secret/index.ts
  • packages/cli/src/lib/app/settings.ts
  • packages/cli/src/commands/access/command.ts
  • packages/cli/src/commands/access/index.ts
  • packages/cli/src/lib/access/settings.ts
  • packages/cli/src/lib/access/record.ts
  • packages/cli/src/lib/app/skill.ts
  • packages/cli/skills/everyapp/SKILL.md
  • packages/cli/src/lib/output/time.ts
  • packages/cli/src/commands/flags.ts
  • packages/cli/src/commands/setup/runSetup.ts
  • packages/cli/src/commands/deploy/index.ts
  • packages/cli/src/commands/deploy/deployApp.ts
  • packages/cli/src/lib/gateway/apply.ts
  • packages/cli/src/lib/migrations/localCli.ts
  • packages/cli/src/lib/migrations/postgres.ts
  • packages/cli/src/lib/migrations/prisma.ts
  • packages/cli/src/lib/app/secrets.ts
  • packages/cli/src/lib/app/deploySecrets.ts
  • packages/cli/src/lib/app/resources.ts
  • packages/cli/src/lib/secretValue.ts
  • packages/cli/src/lib/cf/secrets.ts
  • packages/cli/src/lib/cf/deploy.ts
  • packages/cli/src/lib/cf/runner.ts
  • packages/cli/src/lib/gateway/secretsFile.ts
  • packages/cli/src/lib/gateway/lease.ts
  • packages/cli/src/lib/gateway/remoteMcpApps.ts
  • packages/cli/src/lib/output/prompt.ts
  • packages/cli/src/lib/output/events.ts
  • packages/cli/src/lib/errors.ts
  • packages/perimeter/src/manifest/manifest.ts
  • apps/every-app-gateway/src/env.d.ts
  • apps/every-app-gateway/src/db/remote-mcp.schema.ts
  • apps/every-app-gateway/cloudflare.config.ts
  • apps/every-app-gateway/src/optional-secrets.test.ts
  • apps/every-app-gateway/src/server/oauth-flag.ts
  • apps/every-app-gateway/src/server/mcp-auth/config.ts
  • apps/every-app-gateway/src/server/mcp-auth/maintenance.ts
  • apps/every-app-gateway/src/server/toolbus/gateway-mcp.ts
  • apps/every-app-gateway/src/server/ai-proxy.ts
  • apps/every-app-gateway/src/server/platform-policy.ts
  • apps/every-app-gateway/src/server/deploy/browser-install.ts
  • apps/every-app-gateway/src/server/deploy/cfRestDeployer.ts
  • apps/every-app-gateway/src/server/deploy/metadata.ts
  • apps/every-app-gateway/src/server/deploy/run.ts
  • apps/every-app-gateway/src/server/deploy/errors.ts
  • apps/every-app-gateway/src/server/deploy/auth.ts
  • apps/every-app-gateway/src/server/user-pat-scopes.ts
  • apps/every-app-gateway/src/server/services/UserPatService.ts
  • apps/every-app-gateway/src/server/app-gateway-entrypoint.ts
  • apps/every-app-gateway/src/server/integration-proxy.ts
  • apps/every-app-gateway/src/server/capabilities/members.ts
  • apps/every-app-gateway/src/server/capabilities/oauthClient.ts
  • apps/every-app-gateway/src/server/capabilities/email.ts
  • apps/every-app-gateway/src/server/email/sendEmail.ts
  • apps/every-app-gateway/src/auth/cloudflare-access/config.ts
  • apps/every-app-gateway/src/auth/cloudflare-access/index.ts
  • docs/capabilities.md
  • docs/runbooks/key-rotation.md
  • docs/runbooks/browser-app-install.md
  • docs/runbooks/hosted-install.md
  • docs/security-model.md

On this page