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
statussays about the setting above its first name:<Label>: <why>, the provider'shint(what the key is and where it comes from),Get it at <docs>when the registry or manifest knows where, andGuide: .claude/skills/everyapp/setup/<name>.mdwhen it has a guide (Guide: pnpm everyapp provider show <id>for a provider registered on the Gateway). A plain setting (EMAIL_FROMand the others under Plain settings are shown and checked) gets a visible prompt withTakes …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 asNAME (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):
- The prompts above, when a person can be asked (stdin and stderr are terminals, not
--json,CIunset:canPromptInTerminal). NAME=valuelines on stdin, one per line (parsePipedLinesinpackages/cli/src/commands/configure/index.ts): what a command produced, or a file the person wrote. Blank lines and lines starting with#are skipped; anexportprefix 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_KEYandEMAIL_REST_API_TOKENforemail) is taken too, though never prompted for. Each line is checked before anything is written: a line that isn'tNAME=valueisPIPED_LINE_INVALID(Line <n> of the piped input isn't NAME=valuethenNothing was set.; fixpipe NAME=value lines, one per line; a bare value goes to pnpm everyapp secret set NAME; the line is never echoed, since theecho "$KEY" | …habit pipes a bare secret); a name the manifest doesn't declare isSECRET_NOT_DECLARED(<app> doesn't declare NAMEwhen the name looks like one, upper snake case; otherwise<app> doesn't declare line <n> (not shown: it may be a value); thenThe piped lines set what its deployed manifest lists (a provider, a capability or one of its secrets). Nothing was set.,its everyapp.config.ts listsin the app's directory; fixleave 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 isSECRET_EMPTY(NAME is empty in the piped lines; fixgive NAME a value, or leave the line out); a plain setting's value the Gateway wouldn't read isSETTING_VALUE_INVALID, as fromsecret set; one of the app's own before its first deploy isAPP_NOT_FOUND(<app> isn't deployed yetthenIts own NAME can be set after its first deploy. Nothing was set.; rerunpnpm everyapp deploy).--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 onceWhat 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.
- 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 (isSettableGatewaySettinginpackages/sdk/src/internal/gatewaySettings.ts). A name a provider registered on the Gateway holds (a row of itsproviderstable, 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. - Any other name needs the app's manifest:
everyapp.config.tsin the current directory (cwd), or with--app <id>the manifest the registry stored at the app's last deploy (given both,--appwins and the directory is never read). If that manifest lists the name undersecrets, it goes to the app's Worker and each helper Worker. (unsetalso 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.) - 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. |
- 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 withAPP_NOT_FOUND:<id> isn't deployed yetthenIts secrets can be set after its first deploy. Nothing was changed.; rerunpnpm everyapp deploy. - Only now is each name's value read (one prompt per name, in the order given).
- The change is applied under leases (
gateway.secrets, thenapp.secrets;acquireGatewayLeaseandacquireAppLeaseinpackages/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 asprogressongateway.leaseorapp.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 withLEASE_HELDbefore anything changes:A change to the Gateway's settings is still running after 5morA secret change on <id> is still running after 5m(roughly()inpackages/cli/src/lib/output/time.tsrenders the wait as5m), thenIt's from <who> on <machine>. If it stopped without cleaning up, it's released within <time>.; retryable,data.holder, rerunonce it's done (pnpm everyapp status shows it). The command never callslease.check(), so it doesn't raiseLEASE_LOST; a heartbeat renews each lease every 30 seconds until it is released. For the Gateway, the install row must exist (GATEWAY_NOT_INSTALLED, rerunpnpm everyapp setup). Every target Worker is checked first (assertReplaceableinpackages/cli/src/lib/cf/deploy.ts): if the version serving is not one everyapp recorded, an unattended run stops withaction_required(rerun_interactive: "… was changed outside everyapp; confirm in a terminal") and a person is asked "Set NAME on it anyway?". Then onecf 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'slive_version_id, or the app's resource records), so the nextdeployorsetupreplaces 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:trueorfalse, any case;1,0,on,off,yesandnoare honoured too. The prompt says which one turns the feature off (false).HOSTED_OPEN_SIGNUP:trueorfalse.BROWSER_APP_INSTALLS:onoroff, in lowercase (the Gateway compares it as typed).EVENTS_ALLOW_PRIVATE_TARGETS:trueorfalse, in lowercase (the Gateway turns it on for exactlytrue).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, orName <address>(parseEmailSender).EMAIL_PROVIDER:cloudflareorresend, 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 withouthttps://. Both as the Gateway'saccessUrltakes 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'sthirdPartyDomainreads 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.
STATUSis the SETTINGS vocabularystatusshares (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 recordedfor an app secret whose names the CLI never recorded,! couldn't checkwhen cf couldn't list a Worker. Before the first deploy the app's own rows readafter 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
statusdescribes it (describeUnmetSettings):<label>: <why>(the names when the manifest gives no why and the label doesn't name them), the provider'shint(what the key is and where it comes from, from its entry),Get it at <docs>when the registry or manifest knows where, andGuide: .claude/skills/everyapp/setup/<name>.mdonce per distinct guide; a provider registered on the Gateway has no guide file, and its line isGuide: pnpm everyapp provider show <id>.--jsoncarries the same as each setting'shintandsetup. - 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/ |
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, |
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 |
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 |
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, |
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 / |
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/ |
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 |
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/ |
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_KEYforstripe), 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 undersecrets.provider addrefuses anything else withPROVIDER_SECRET_TAKEN, and the Gateway skips such a row on every read. - The routing.
pnpm everyapp secret set STRIPE_API_KEYfrom 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 printsSTRIPE_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 answerintegration_not_configureduntilsecret setruns again.provider removeleaves the setting on the Gateway:secret unsetremoves 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.
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 addnames 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 setsends 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 (kindremote-mcp, setup guidesetup/remote-mcp.md); a server added with--auth noneneeds nothing. - Setting it.
pnpm everyapp secret set TREG_MCP_TOKEN, ormcp additself: at a terminal it asksSet TREG_MCP_TOKEN now?and reads the value with the hidden prompt;mcp add --from-env VARreads 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 setwrites(name, origin, bound_by, bound_at)toremote_mcp_credentialsin 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 runssecret setagain. 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 runssecret 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 (progresssecrets.pin), not raised: the value is live but unpinned, so runsecret setagain. - 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 addrefuses a second origin withREMOTE_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_TOKENremoves the value and its pin.mcp removeremoves the app and leaves both. - Listing.
secret listnames 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 itssecret set, and carries every credential indata.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_KEYandJWT_PUBLIC_KEY(RS256, 2048-bit, PKCS#8 and SPKI PEM),GATEWAY_DEPLOYMENT_MODE(self_hostedon a Worker's first upload;hostedis the only other valueapps/every-app-gateway/src/server/platform-policy.tsaccepts, the hosted Gateway this page refers to, and no command sets it any more; anything else, unset included, isinvalidand 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) andCLOUDFLARE_ACCOUNT_IDwithcf deploy --secrets-file(packages/cli/src/lib/gateway/secretsFile.ts).APP_BASE_DOMAINis a text binding the deploy patches into the Gateway's Build Output (packages/cli/src/lib/gateway/patchBuildOutput.ts). THIRD_PARTY_DEPLOYSis the hosted launch gate: exactlyonopens the third-party deploy API. The CLI never sets it: set it by hand on theevery-app-gatewayWorker'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'ssecretsand 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 withnode 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#1BEGIN RSA PRIVATE KEYprivate key deploys without error and fails at runtime), replaceJWT_PRIVATE_KEYandJWT_PUBLIC_KEYtogether 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), runpnpm everyapp setup --redeployin a terminal, thenpnpm everyapp deployfrom every app's directory, since each app carries the public key inEVERYAPP_IDENTITY_PUBLIC_KEYS. Until every app is redeployed, apps with the old key answer401to new identities: the SDK tries each key inEVERYAPP_IDENTITY_PUBLIC_KEYSand throwsIdentityError(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 withIDENTITY_KEY_UNKNOWNis 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 currentJWT_PUBLIC_KEY. Every other key (a provider's, a client's, an app's own) rotates withpnpm everyapp secret set NAMEagain: 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
- Deploy the app once (
pnpm everyapp deploy): the production build declares no app secret, and the upload carries onlyGATEWAY_URLandEVERY_APP_ORG_ID. pnpm everyapp configure todoasks for each, from anywhere (pnpm everyapp configure todo --from-env --jsonfor an agent). One name at a time, from the app's directory:pnpm everyapp secret set GITHUB_TOKEN(a person at the prompt) orpnpm 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, thesecretsfield), andcf deploy(the CLI's pinnedcf, 1.0.0-beta.14) uploads every version withkeepSecretson, which putssecret_textandsecret_keyin the upload'skeep_bindings, so a secret absent from the Build Output and the--secrets-filestays on the Worker.- In the Worker,
env.GITHUB_TOKENis the string. Type it as optional, since it is absent in development until set locally, by adding to the app'ssrc/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).
pnpm everyapp secret unset GITHUB_TOKENremoves it from every Worker that holds it. Dropping the name from the manifest does not remove the value:unsetstill 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
providersandcapabilities(DEV_GATEWAY_KEYS:OPENAI_API_KEY,OPENROUTER_API_KEY,WORKERS_AI_API_TOKENwithCLOUDFLARE_ACCOUNT_ID,DATAFORSEO_API_KEY,CONTEXT_API_KEY, each OAuth client's id and secret andMASTODON_URL, email'sEMAIL_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_SENDERSwhen the manifest listsemail(the senders standing in foreveryapp 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 throwsConfigurationError: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 throwsSet OPENAI_API_KEY in .dev.vars to call openai in local development.Otherwise the call goes to the provider's origin withAuthorization: Bearer <key>; Workers AI's path is joined under/client/v4/accounts/<id>/aiand its stream normalized as the Gateway does. Only/api/ai/openai,/api/ai/openrouterand/api/ai/workers-aipaths 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 aTypeError. 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 answersrequested: trueandconfigured/readytrue whenEVERYAPP_DEVand the key are set (an OAuth client: its id and secret;email:EMAIL_FROM;members:EVERYAPP_DEValone).
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
cfonly in private temp files (0600, removed afterwards) or the environment, never argv or output;.everyapp/secrets.envvalues never reach a Worker. secret set|unsetchange 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-cookieand 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|offwrites them as Worker secrets too).CF_DEPLOY_TOKENon 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.tspackages/sdk/src/internal/remoteMcp.tspackages/sdk/src/internal/appSettings.tspackages/sdk/src/internal/aiProviders.tspackages/sdk/src/internal/capabilities.tspackages/sdk/src/internal/email.tspackages/sdk/src/internal/identity.tspackages/sdk/src/internal/protocol.tspackages/sdk/src/internal/oauthClient.tspackages/sdk/README.mdpackages/sdk/src/cloudflare/worker.tspackages/sdk/src/server/gatewayFetch.tspackages/sdk/src/server/integrationFetch.tspackages/sdk/src/server/attribution.tspackages/sdk/src/server/everyApp.tspackages/sdk/src/env.d.tspackages/cli/src/commands/configure/command.tspackages/cli/src/commands/configure/index.tspackages/cli/src/commands/secret/command.tspackages/cli/src/commands/secret/index.tspackages/cli/src/lib/app/settings.tspackages/cli/src/commands/access/command.tspackages/cli/src/commands/access/index.tspackages/cli/src/lib/access/settings.tspackages/cli/src/lib/access/record.tspackages/cli/src/lib/app/skill.tspackages/cli/skills/everyapp/SKILL.mdpackages/cli/src/lib/output/time.tspackages/cli/src/commands/flags.tspackages/cli/src/commands/setup/runSetup.tspackages/cli/src/commands/deploy/index.tspackages/cli/src/commands/deploy/deployApp.tspackages/cli/src/lib/gateway/apply.tspackages/cli/src/lib/migrations/localCli.tspackages/cli/src/lib/migrations/postgres.tspackages/cli/src/lib/migrations/prisma.tspackages/cli/src/lib/app/secrets.tspackages/cli/src/lib/app/deploySecrets.tspackages/cli/src/lib/app/resources.tspackages/cli/src/lib/secretValue.tspackages/cli/src/lib/cf/secrets.tspackages/cli/src/lib/cf/deploy.tspackages/cli/src/lib/cf/runner.tspackages/cli/src/lib/gateway/secretsFile.tspackages/cli/src/lib/gateway/lease.tspackages/cli/src/lib/gateway/remoteMcpApps.tspackages/cli/src/lib/output/prompt.tspackages/cli/src/lib/output/events.tspackages/cli/src/lib/errors.tspackages/perimeter/src/manifest/manifest.tsapps/every-app-gateway/src/env.d.tsapps/every-app-gateway/src/db/remote-mcp.schema.tsapps/every-app-gateway/cloudflare.config.tsapps/every-app-gateway/src/optional-secrets.test.tsapps/every-app-gateway/src/server/oauth-flag.tsapps/every-app-gateway/src/server/mcp-auth/config.tsapps/every-app-gateway/src/server/mcp-auth/maintenance.tsapps/every-app-gateway/src/server/toolbus/gateway-mcp.tsapps/every-app-gateway/src/server/ai-proxy.tsapps/every-app-gateway/src/server/platform-policy.tsapps/every-app-gateway/src/server/deploy/browser-install.tsapps/every-app-gateway/src/server/deploy/cfRestDeployer.tsapps/every-app-gateway/src/server/deploy/metadata.tsapps/every-app-gateway/src/server/deploy/run.tsapps/every-app-gateway/src/server/deploy/errors.tsapps/every-app-gateway/src/server/deploy/auth.tsapps/every-app-gateway/src/server/user-pat-scopes.tsapps/every-app-gateway/src/server/services/UserPatService.tsapps/every-app-gateway/src/server/app-gateway-entrypoint.tsapps/every-app-gateway/src/server/integration-proxy.tsapps/every-app-gateway/src/server/capabilities/members.tsapps/every-app-gateway/src/server/capabilities/oauthClient.tsapps/every-app-gateway/src/server/capabilities/email.tsapps/every-app-gateway/src/server/email/sendEmail.tsapps/every-app-gateway/src/auth/cloudflare-access/config.tsapps/every-app-gateway/src/auth/cloudflare-access/index.tsdocs/capabilities.mddocs/runbooks/key-rotation.mddocs/runbooks/browser-app-install.mddocs/runbooks/hosted-install.mddocs/security-model.md
Identity, headers and the SDK
What an app Worker receives from the Gateway on every request (headers, the identity token and its claims, env and bindings), what @every-app/sdk/server exports, and how the app calls the Gateway back for AI, capabilities, email and members.
Development vs production
What everyapp dev starts and in what order, the seeded dev identity and why it is loopback-only, which perimeter rules are identical to production and what has no Gateway behind it locally (AI, capabilities, email, members, secrets, D1, MCP), which .dev.vars names the ready block's Dev vars line asks for, and why only everyapp deploy makes a production build.