EveryAppEveryApp

API providers: the registry and the catalog

What an API provider is (beside an MCP app), the built-in catalog and the providers one Gateway registers, the spec's JSON fields and their rules, the request each auth kind makes, what a registered row may say, everyapp provider add, list, show and remove with their --json data, the codes, local development, and every catalog provider.

An API provider is an API the Gateway holds a key for and brokers calls to (DataForSEO, Context.dev, Stripe): an app lists it under capabilities in everyapp.config.ts and calls the provider's real URLs with integrationFetch(), and the Gateway puts the key on the call, so no app ever holds it. An MCP app (pnpm everyapp mcp on dataforseo, /docs/agent/mcp) is something else: an app of the Gateway's own with no page, whose tools members' AI clients call on /mcp. An MCP app is built on an API provider's entry (DataForSEO's tools call DataForSEO through the same proxy), and nothing on this page changes MCP apps. The design and its decisions are in the repository's docs/design/2026-10-provider-registry.md.

The catalog and a Gateway's rows

Every provider the Gateway knows comes from one of two places, resolved as one lookup, the registry (providerRegistry() in packages/sdk/src/internal/providerRegistry.ts):

Source What Who changes it How the Gateway reads it
The built-in catalog CAPABILITIES in packages/sdk/src/internal/capabilities.ts, in every Gateway and every SDK: the API-key providers, the AI providers, the OAuth clients and the Gateway's own services (the table at the end) A pull request; the tests in packages/cli/src/docs hold the docs to it Statically
A Gateway's rows The providers table in the Gateway's D1 (apps/every-app-gateway/drizzle/0013_providers.sql): id, spec (the JSON below), added_by, added_at, updated_at, bound_to, bound_at The operator, with pnpm everyapp provider add, under the Cloudflare login apps/every-app-gateway/src/server/providers/index.ts, cached 30 seconds per isolate: every row parsed and checked again on every read, and a row that fails is left out and logged provider.row_invalid (the id, the reason and the parser's message, never the row's spec)

The catalog comes first and a row can never shadow it, so nothing a row says changes where a catalog provider's key goes. A row is the Gateway's, not an organization's: its key is one Gateway setting shared by every app and every organization on that Gateway, as a catalog provider's is. A Gateway whose D1 predates migration 0013, or whose rows can't be read, serves the catalog alone (logged provider.rows_unavailable, once per isolate).

The catalog has four kinds (mode in the code). Only the first is a spec a Gateway can register:

Kind What the Gateway does An app lists it under The app calls
api_key Puts its key on calls to the provider's own hosts, with the methods the spec allows capabilities integrationFetch({ env, provider, for }) on the provider's URLs
ai Puts its key on inference calls (the AI proxy's rules: inference routes only, store: false, the model allowlist). Never a capability: a key on an inference host reaches the account's files, batches and keys too providers createGatewayFetch({ env, for }) on /api/ai/<id>/…
oauth_client Holds the OAuth client and completes the provider's token endpoints only; the app keeps people's tokens capabilities integrationFetch on the token endpoint
service Does the work itself (email, members): no host, no spec capabilities sendEmail(), listMembers()

An AI provider under capabilities is refused at deploy (capabilities.openrouter: openrouter is an AI provider: list it under providers, not capabilities), and /api/integrations/openrouter/… answers 400 invalid_integration_route.

How an app lists one

Nothing in the manifest is new. A registered provider is listed like a catalog one, with the need fields of the manifest page:

capabilities: {
  stripe: { access: "read", why: "Shows the last ten charges on the dashboard." },
},

access: "write" takes effect only on a provider whose spec offers write: it unlocks the spec's writeMethods, and a call under an agent's identity (chan: "mcp") still needs the mcp:write scope for them (integration_read_only). The app calls it with integrationFetch({ env, provider: "stripe", for: request }): an id outside the catalog is accepted, sent over the EVERY_APP_GATEWAY binding with only an https check, and the Gateway's checks decide.

everyapp deploy checks the manifest against the Gateway's registry once connected, so a registered provider's entry is checked like a catalog one (the access it offers, no scope sets), and an id neither the catalog nor the rows have is CAPABILITY_UNKNOWN, whose fix is pnpm everyapp provider add <spec>. An app's secrets may not name a row's setting either (the validator's wording for a Gateway name). everyapp dev can't ask the Gateway: it accepts an id outside the catalog and warns CAPABILITY_NOT_IN_CATALOG for each (local development).

The spec

One JSON object, the same shape every catalog entry has in TypeScript (parseProviderSpec in packages/sdk/src/internal/providerSpec.ts parses it; providerSpecOf writes a catalog entry back as one):

{
  "id": "stripe",
  "name": "Stripe",
  "description": "Payments: customers, subscriptions, invoices.",
  "docs": "https://dashboard.stripe.com/apikeys",
  "api": "https://docs.stripe.com/api",
  "hosts": ["api.stripe.com"],
  "auth": { "kind": "bearer", "secret": "STRIPE_API_KEY" },
  "secrets": [
    {
      "name": "STRIPE_API_KEY",
      "hint": "A restricted key with read access to Customers and Charges is enough."
    }
  ],
  "access": ["read", "write"],
  "readMethods": ["GET", "HEAD"],
  "writeMethods": ["POST", "PUT", "PATCH", "DELETE"],
  "accountWidePaths": [
    ["v1", "account"],
    ["v1", "api_keys"]
  ],
  "data": "account",
  "hint": "A restricted key from the Stripe dashboard; the Gateway puts it on every call as a bearer token.",
  "smoke": {
    "method": "GET",
    "path": "/v1/balance",
    "expect": [200],
    "free": true
  },
  "cost": {
    "unit": "call",
    "usd": 0,
    "note": "Free; Stripe charges per transaction, not per call."
  },
  "rate": { "perSecond": 100, "note": "Live mode; test mode 25/s." },
  "example": "list my latest Stripe customers",
  "verified": { "on": "2026-10-25", "how": "docs" },
  "tags": ["payments"]
}

A spec names Gateway settings and never holds a value: no field takes one. Every field, with the parser's rule; any other field, anywhere in the object, is unknown field, and no text may hold a control character:

Field Required Rule When absent
id yes Lowercase letters, digits and hyphens, a letter first, 2 to 40 characters (^[a-z][a-z0-9-]{1,39}$). A row may not take a catalog id or everyapp
mode no "api_key", or left out: the kind comes from auth.kind. An AI provider is a catalog entry, not a spec a Gateway takes; an OAuth provider is a spec whose auth.kind is oauth2-user (below) api_key
name yes 1 to 64 characters: the provider's name in lists, errors and logs
description no At most 200 characters
docs yes An https URL, at most 500 characters: where the person gets the key
api no An https URL: the API reference
hosts yes, unless hostSetting At most 20 bare lowercase hosts with a dot and no scheme, port or path (api.stripe.com), none twice: the only hosts the key is ever sent to
hostSetting no A setting name holding the host, for a provider whose host is the operator's (below); another setting than the key
data no "per-call" or "account" (below) neither
auth yes Where the key goes (auth kinds)
secrets no [{ name, hint? }]: each name one of the settings auth or hostSetting names, once; hint at most 300 characters about that setting
access no ["read"], ["write"] or both: what an app's entry may ask for ["read"]
readMethods no A non-empty list of GET, HEAD, POST, PUT, PATCH, DELETE: what any grant may use (a provider whose reads are POSTs lists POST) ["GET", "HEAD"]
writeMethods no The same methods: what access: "write" adds, on a spec whose access offers write ["POST", "PUT", "PATCH", "DELETE"]
accountWidePaths no At most 50 runs of lowercase path segments ([["v1", "account"]]), refused wherever the segments appear together in a path, however spelled: endpoints that list or describe the whole account the key belongs to []
hint no At most 500 characters: what the key is and where it comes from, shown beside why wherever setup is (hint)
smoke no { method, host?, path, expect, free, note? }: one request that shows the key works. method GET, HEAD or POST; host one of hosts (the first when absent); path starting with one /; expect the statuses that mean it works; free true or false the catalog table marks it "none recorded"
cost no { unit, usd?, note?, responseField? }: unit call, result or token; usd zero or more; responseField the JSON field of the answer that reports the call's cost in US dollars, read into the usage counters: a top-level name the capability proxy reads off the answer's head (DataForSEO's cost), or a dotted path the AI proxy reads off the tail with the token usage (OpenRouter's usage.cost); without it usd is the flat per-call price no cost is read
rate no { perSecond?, perMinute?, perDay?, note? }: the provider's limits, as hints
example no At most 200 characters: what to say to an agent to try it
verified no false, or { "on": "YYYY-MM-DD", "how": "docs" | "live" }: whether someone checked the spec against the provider
tags no At most 10 lowercase words

Setting names (auth.secret, auth.user.secret, hostSetting, secrets[].name) match ^[A-Z][A-Z0-9_]{2,63}$. The parser reports every problem at once, each with its path, joined in the order of the table above as PROVIDER_SPEC_INVALID's message: hosts.0: must be a bare lowercase host with a dot and no port, path or scheme (api.stripe.com); auth.secret: must name a Gateway setting: upper case letters, digits and underscores, a letter first, 3 to 64 characters (STRIPE_API_KEY).

Auth kinds: the request each makes

secret is always the name of a Gateway setting; the value is read from the Gateway's settings at the call, trimmed. While a setting auth names is unset or blank, calls answer 503 integration_not_configured.

auth What the Gateway puts on the request Example
{ "kind": "bearer", "secret": "STRIPE_API_KEY" } Authorization: Bearer <STRIPE_API_KEY>; "scheme": "Token" (a token of letters, digits and hyphens) replaces the word Stripe, Context.dev, OpenAI
{ "kind": "header", "secret": "X_API_KEY", "header": "x-api-key", "prefix": "" } <header>: <prefix><value>. header is letters, digits and hyphens, never one the Gateway sets or strips itself (host, cookie, content-length, content-type, transfer-encoding, connection, upgrade, te, trailer, keep-alive, proxy-authorization, proxy-authenticate, forwarded, x-forwarded-for, x-forwarded-host, x-forwarded-proto, x-everyapp-*); prefix at most 32 characters Anthropic's x-api-key, Klaviyo's Authorization: Klaviyo-API-Key <key>
{ "kind": "query", "secret": "X_API_KEY", "param": "appid" } ?appid=<value> on the URL, replacing every copy of the parameter the app sent. The key is in the URL, so a 3xx answer is never relayed (502 integration_upstream_redirected), location, content-location and refresh are deleted from every answer, and a failed call logs the error's name only OpenWeather, Mapbox
{ "kind": "basic", "secret": "X_API_KEY", "as": "password", "user": "api" } Authorization: Basic base64(api:<value>): the key is the password beside a literal user (no colon, at most 128 characters) Mailgun
{ "kind": "basic", "secret": "X_AUTH_TOKEN", "as": "password", "user": { "secret": "X_ACCOUNT_SID" } } Authorization: Basic base64(<X_ACCOUNT_SID>:<X_AUTH_TOKEN>): the user is a second setting, another than secret Twilio
{ "kind": "basic", "secret": "X_API_KEY", "as": "username" } Authorization: Basic base64(<value>:): the key is the username, the password empty Segment, Mixpanel, Crisp
{ "kind": "basic", "secret": "X_API_KEY", "as": "pair" } Authorization: Basic <…>: the value is login:password, base64-encoded, or sent as it is when it holds no colon (already encoded) DataForSEO

as is required for basic and says where the key goes, never guessed from the value; user belongs to as: "password" only. The fifth kind, oauth2-user, is an OAuth provider the Gateway runs the user flow for (below); its auth names the two client settings instead of a secret. Any other kind is refused: an AI provider is a catalog entry; a client-credentials exchange and signed requests (AWS SigV4, HMAC) aren't in this slice. A fixed header the provider wants on every call (anthropic-version, Notion-Version) is the app's to send: the Gateway forwards an app's ordinary headers. Every request also loses the app's Authorization, x-api-key, cookies, x-everyapp-* and forwarding headers before the key goes on, and is made with redirect: "manual".

hostSetting: a host of the operator's

For a provider whose host is per account (Shopify's <shop>.myshopify.com), the spec names hostSetting, a Gateway setting under the provider's prefix like its key (SHOPIFY_HOST), instead of or beside hosts. Its value is a bare host or an https origin with nothing else (no path, port, query or userinfo), and the key may go to that host as well as hosts; without a value the provider has no host there and the call is 400 invalid_integration_route. The value's shape is checked when read (hostSettingValue in packages/sdk/src/internal/capabilities.ts); the operator who sets it decides the host.

data: whose data the key reaches

"per-call": each call is its own (search, scrape, inference, weather). "account": every list is the whole account the key belongs to (Stripe, Airtable, Linear). One key is every organization's on a Gateway, so a hosted Gateway refuses an account provider (503 integration_not_available_hosted) until it holds per-organization keys; a self-hosted Gateway, one organization, serves it.

What a row may say

Registering a provider gives the Gateway a new host to send a key to. These rules keep that from sending any other key anywhere, run by provider add before it writes and by the Gateway every time it reads a row (checkProviderRow and providerRegistry in packages/sdk/src/internal/providerRegistry.ts); each is a row of the threat model's register (B4):

Invariant The rule Refused as
providers.row-secret-is-its-own Every setting a row names starts with its id upper-cased, - as _ (STRIPE_ for stripe, TIKTOK_BUSINESS_ for tiktok-business), and is nobody else's: not one of the Gateway's own keys (JWT_PRIVATE_KEY, BETTER_AUTH_SECRET, …), a local-development setting, a name everyapp sets itself (EVERYAPP_*, EVERY_APP_*, APP__*), a settable Gateway setting (every catalog key and client, MCP_*, CLOUDFLARE_ACCESS_*, EMAIL_*), another row's (the earlier row keeps it), or a name a registered app lists under secrets (the message names the app) PROVIDER_SECRET_TAKEN
providers.row-hosts-public-https Each host is a public DNS name: not an IP address, localhost, or under .localhost, .local, .internal or .arpa; and not the Gateway's own host or under its APP_BASE_DOMAIN or THIRD_PARTY_DOMAIN. Calls go over https only PROVIDER_HOST_REFUSED
providers.builtin-precedence A row never takes a catalog id or everyapp; the catalog is read first; a row a later catalog entry collides with is skipped and logged, never served PROVIDER_BUILTIN
providers.spec-names-no-values The parser refuses unknown fields and control characters; header and scheme are tokens; a stored spec is parsed again on every read PROVIDER_SPEC_INVALID
providers.value-bound-to-hosts A row's key is read only for the hosts and auth it was set for (below) integration_not_configured
providers.add-is-privileged Rows are written under the Cloudflare login (ADR-0003), or by an owner or admin through everyapp_provider_add and everyapp_provider_remove on a self-hosted Gateway (hidden on a hosted one); the Gateway has no HTTP route that writes the table, and the tools never replace a row nor remove a bound or listed one (the provider tools)
providers.query-key-never-echoed A query key never comes back to the app: no redirect or URL-naming header is relayed, and a text answer is read whole (4 MiB at most) with every copy of the value replaced before it goes out, since a provider that builds URLs for the app (Mapbox's TileJSON) writes the key into the body (auth kinds) integration_upstream_redirected
providers.account-data-self-host-only An account provider is refused on a hosted Gateway (data) integration_not_available_hosted

The other direction holds too: an app whose secrets names a row's setting is refused at deploy, as for any Gateway setting.

The key is bound to the hosts it was set for

A registered provider's setting is a Gateway setting like any other, set with pnpm everyapp secret set STRIPE_API_KEY from any directory. It isn't in SETTABLE_GATEWAY_SETTINGS (that list is the catalog's), so secret set connects first and asks the rows which provider holds the name. Before it asks for the value it prints whose it is and where it will go:

STRIPE_API_KEY is Stripe's, registered by ops@example.com on 2026-10-25; the key is sent to api.stripe.com.

After the value is set it records on the row what the key was set for: providerBindingKey of the row (its hosts, hostSetting and auth as one canonical string) in bound_to, with bound_at. The Gateway reads the key for a row only while that still matches the row it has. So:

  • A row added and never given a key is not set up: calls answer 503 integration_not_configured, its status configured: false, and every app that lists it shows "Needs setup" (its setting reads as unset in status, secret list and the Gateway's app lists), even when the name happens to be set on the Gateway Worker.
  • A row replaced (provider add --replace), removed and added again, or edited by hand in D1 is unbound until secret set runs again: calls answer 503 integration_not_configured with <Name>'s key was set for other hosts than the provider has now. Its operator sets it again with: everyapp secret set <NAMES>. A person changing a row's hosts sets the key again; nobody who can only rewrite rows can move a key the operator set.
  • provider remove leaves the setting on the Gateway Worker (pnpm everyapp secret unset STRIPE_API_KEY removes it), and nothing reads it until a new row is bound by a new secret set.

OAuth providers: upload a client, the Gateway runs the flow

An OAuth provider is a provider whose credential is a person's: GitHub, Google, Notion, Linear, any API with an OAuth 2 authorization-code flow. Since 2026-10-25 it is a kind of the registry (docs/design/2026-10-oauth-providers.md): an owner or admin adds one from a preset or a spec, pastes the client id and secret the provider issued, and the Gateway runs the whole flow itself, holds each person's tokens sealed, refreshes them, and puts the right token on every call an app makes through integrationFetch(). The app never sees the client secret, the access token or the refresh token; it sees calls that go through and refusals that say what to do.

Two flows exist on the same client, and a catalog entry may offer both (integrationStatus().flow is app, gateway or both):

  • The app-driven flow (app): the social networks' entries since #326. The app builds the authorize URL from the status's clientId, receives the callback on its own host, exchanges the code through the Gateway's token-endpoint proxy and keeps the person's tokens itself. Unchanged.
  • The Gateway-owned flow (gateway, this section): the person connects once, in the Gateway, and every app that lists the provider calls it through the proxy with that connection. google-workspace, microsoft-365, slack and discord offer both; every registered OAuth provider and every preset offers this one.

The spec: auth.kind: "oauth2-user"

The same JSON a row of any kind is, with auth an OAuth block and a scopeSets object; parseProviderSpec takes it, and oauthSpecOf writes a catalog entry that has a flow back as one (provider show <id> prints it):

{
  "id": "github-oauth",
  "name": "GitHub",
  "description": "Repositories, issues, pull requests and the person's profile.",
  "docs": "https://github.com/settings/developers",
  "api": "https://docs.github.com/rest",
  "hosts": ["api.github.com"],
  "auth": {
    "kind": "oauth2-user",
    "authorizeUrl": "https://github.com/login/oauth/authorize",
    "tokenUrl": "https://github.com/login/oauth/access_token",
    "revokeUrl": "https://api.github.com/applications/{client_id}/grant",
    "revokeMethod": "DELETE",
    "clientId": "GITHUB_OAUTH_CLIENT_ID",
    "clientSecret": "GITHUB_OAUTH_CLIENT_SECRET",
    "inject": "form",
    "pkce": true,
    "scopeDialect": "comma",
    "tokenHeaders": { "Accept": "application/json" },
    "identity": {
      "url": "https://api.github.com/user",
      "label": "login",
      "id": "id"
    }
  },
  "scopeSets": {
    "identity": {
      "label": "GitHub account (who you are)",
      "access": "read",
      "scopes": ["read:user", "user:email"]
    },
    "repo.read": {
      "label": "Repositories (read, private ones included)",
      "access": "read",
      "scopes": ["repo"]
    },
    "repo.write": {
      "label": "Repositories (read and write)",
      "access": "write",
      "scopes": ["repo"]
    },
    "org.read": {
      "label": "Organizations and teams (read)",
      "access": "read",
      "scopes": ["read:org"]
    }
  },
  "access": ["read", "write"],
  "hint": "Register an OAuth App (not a GitHub App) and paste its client id and secret."
}

The common fields (id, name, description, docs, api, hosts, access, readMethods, writeMethods, hint, example, tags) follow the table above; docs is where the person makes the OAuth app (the console). hosts are the API hosts the person's token is sent to; the authorize, token and revoke hosts need not be among them. The OAuth fields, with the parser's rule; any other field under auth is not a field of auth.kind oauth2-user:

Field Required Rule When absent
auth.authorizeUrl yes An https URL with no port, credentials, fragment or query string, on a host that passes the row host rules (public DNS, never the Gateway's own)
auth.tokenUrl yes The same; the token request is a POST there
auth.revokeUrl no The same; {client_id} in the path is replaced by the client id (GitHub's grant endpoint). Without it, disconnecting deletes the Gateway's copy and revokes nothing at the provider no revocation at the provider
auth.revokeMethod no POST or DELETE; needs a revokeUrl POST
auth.clientId yes A setting name under the row's prefix (GITHUB_OAUTH_ for github-oauth): where the client id lives when set from a terminal
auth.clientSecret no Another setting name under the prefix. Absent: a public client (PKCE alone) public client
auth.inject no form (the client id and secret in the token request's body) or basic (HTTP Basic) form
auth.pkce no true or false: whether the authorize request carries code_challenge (S256) and the token request its verifier. Off only for a provider that refuses the parameter true
auth.scopeDialect no How the provider spells granted scopes in its token answer: space (RFC 6749), comma (GitHub), google, microsoft or slack (the catalog's parsers). For space and comma, an answer with no scope field is taken as granted-as-requested (RFC 6749 §5.1) space
auth.authorizeParams no At most 10 extra query parameters on the authorize URL (Google's access_type=offline and prompt=consent); never client_id, redirect_uri, response_type, scope, state, code_challenge or code_challenge_method, which the Gateway sets
auth.tokenHeaders no At most 10 extra headers on the token request (GitHub's Accept: application/json); never authorization, content-type, content-length, host, cookie or an x-everyapp-* name
auth.identity no { url, label, id? }: after the exchange the Gateway GETs url (on one of hosts) with the new token and reads label and id (a field name, or a dotted path at most four deep) to name the connection ("ben" at GitHub, the email at Google) the connection is named by its provider
scopeSets yes 1 to 20 sets named identity or <area>.<access> (repo.read), each { label, access, scopes, userScopes?, routes? }: access read or write; scopes at most 40 provider scope strings, one each, no space or comma inside; routes at most 20 of { host, paths, posts? } on one of hosts, as the scopes design's route sets
alwaysRequested no Scopes every connection asks for whatever its sets (offline_access) []
tolerated no Scopes a token may carry beyond the sets without refusal (identity and refresh scopes in every spelling the provider uses); never a data scope []

A set with routes unlocks those paths only (GET and HEAD on paths, the posts patterns); a set without routes unlocks every path on hosts for its access (a read set the readMethods, a write set the write methods too). On every host the segments batch and $batch never pass. The parsed spec is the same OAuthClientCapability a catalog client is, with a scopePolicy built from the sets (grantTypes authorization_code and refresh_token, requestScope: "keep") and a flow block carrying the rest; isOAuthProvider() is true for it.

The row rules of the table above hold with the client settings in place of the key, and one more: oauth.client-bound-to-endpoints. A client uploaded for a row stays usable only while the row still has the authorize, token and revoke URLs, hosts, inject and setting names it was uploaded for (providerBindingKey over those); a row replaced since reads as having no client until the client is pasted or set again, and the token-endpoint branch and the status refuse an unbound OAuth row too.

Presets

OAUTH_PRESETS (packages/sdk/src/internal/oauthPresets.ts) are specs of this shape shipped with the SDK, one per provider: github-oauth, notion-oauth, linear-oauth, gitlab-oauth, atlassian (Jira and Confluence), hubspot-oauth, zoom, dropbox, spotify and x-oauth (X's OAuth 2 with PKCE; X's OAuth 1.0a stays an app secret). An id carries -oauth where the catalog already holds an API-key entry of that name, so github (a token) and github-oauth (a person's grant) are two providers with two setting families. A preset becomes a row when added (provider add --oauth github-oauth, the dialog, everyapp_provider_add with { preset }), so provider show github-oauth prints it and a copy can be edited and added again under another id; a preset is a starting point, never a hidden catalog entry. Each is parsed by a test, and the catalog table below lists them with the catalog's clients. The catalog's own OAuth entries that have a flow, google-workspace, microsoft-365, slack and discord, are offered in the same picker, marked built in: adding one uploads the client only and writes no row.

The client: pasted in the Gateway, or set from a terminal

One client per provider per Gateway, as a row's key is. Two places can hold it, and the first that is set wins:

  1. Worker secrets, set from a terminal: pnpm everyapp secret set GITHUB_OAUTH_CLIENT_ID GITHUB_OAUTH_CLIENT_SECRET (the row's two settings; GOOGLE_WORKSPACE_CLIENT_ID and its secret for the catalog's Google), which binds the row as any key does. The CLI never writes the sealed copy: the key that seals it lives in the Worker.
  2. The oauth_clients table, pasted in Manage workspace → Providers (the Add OAuth provider dialog, or Replace client): the client id in clear, the secret sealed (AES-256-GCM through WebCrypto, a random nonce per value, stored as v1.<nonce>.<ciphertext>; the key derived once per isolate with HKDF-SHA256 from BETTER_AUTH_SECRET with the info everyapp:oauth:v1, so no new setting is needed; apps/every-app-gateway/src/server/secrets/sealed.ts), with who pasted it and when, and the row's binding key. A sealed value that no longer opens (the secret rotated) reads as no client.

provider list, provider show, the Admin table and everyapp_provider_list say which source is live (env or d1, the client id, who, when) and never a secret. Forget pasted client in the dialog deletes the sealed copy (its twin from a terminal is secret unset of the two settings); Replace client overwrites it. The redirect URI the person registers at the provider's console is https://<gateway>/connect/<id>/callback; every surface prints it beside the console's URL.

The flow

GET /connect/<id>?account=user|organization&return=<path or app URL>&sets=a,b
  the person's own session (any member for `user`; an owner or admin for `organization`,
  the workspace's one account background work uses); the provider must be an OAuth
  provider with a live client (404 or 409 saying why)
  sets: the ones named, else the union of the sets every active app that lists the
  provider declares, else every set the provider has; `identity` always when it exists
  → one `oauth_states` row (32 random bytes as the state, the PKCE verifier, who, which
    organization, the account kind, the sets, where to return; 10 minutes, one use)
  → 302 to authorizeUrl with response_type=code, client_id, redirect_uri, scope (the
    union, in the dialect's separator), state, code_challenge(_method=S256), authorizeParams

GET /connect/<id>/callback?code&state            ← the redirect URI
  the state row is this person's, unexpired, and deleted at once (`oauth.state-single-use`)
  an `error` from the provider ends on a Gateway page, never a redirect
  → POST tokenUrl with code, redirect_uri, code_verifier, the client as `inject` says,
    tokenHeaders; 64 KiB answer, redirect: "manual", never logged
  → the granted scopes (the dialect) must cover the sets asked; an excess beyond
    `tolerated` is refused and the tokens dropped (the scopes design's rules)
  → `identity` names the account; one `provider_connections` row per
    (organization, provider, subject), replaced on reconnect, tokens sealed
  → `provider.connected` with the actor
  → 302 to `return`: a path on the Gateway, or an https URL whose host is one of this
    Gateway's app hosts (APP_BASE_DOMAIN, a registered app's host; http://localhost:<port>
    in GATEWAY_DEPLOYMENT_MODE=dev). Anything else lands on Settings → Connected accounts

An app sends people into it with connectUrl({ env, provider, returnTo, account }) (@every-app/sdk/server: <GATEWAY_URL>/connect/<id>?return=…&account=…; connectPath is the path alone, also on the status). The person lands back where they were, connected.

Refresh. A call through the proxy refreshes a connection whose expires_at is within 60 seconds, under a lease on the row (one conditional UPDATE, so parallel calls don't race); a refresh the provider refuses marks the connection expired, emits provider.connection_expired once, and calls answer integration_not_connected with reconnect: true until the person connects again.

Disconnect. Settings → Connected accounts (own), Manage workspace → Providers (any), pnpm everyapp provider disconnect, everyapp_provider_disconnect: deletes the row, emits provider.disconnected, and revokes at the provider only when the spec has a revokeUrl. The Google entry has none on purpose: Google's revocation is per person per client and would also kill the tokens an app-driven app holds under the same client; the copy the Gateway drops dies at its expiry. GitHub, GitLab, Dropbox and Zoom revoke. The CLI's disconnect deletes without revoking (it has no token to revoke with) and says so.

Calls with the held token

/api/integrations/<id>/<host>/<path> for an OAuth provider, in this order:

  1. The host is one of the provider's token endpoints: the app-driven branch, unchanged (OAuth clients).
  2. The host is in hosts: the held-token branch. The app is active and lists the provider (capability_not_requested); the method is allowed (integration_method_not_allowed: read methods on any grant, write methods on access: "write" for a provider that offers it); the client is live (integration_not_configured); the call is attributed (admit()), and an agent's read-only connection may not write (integration_read_only); the connection: x-everyapp-account (integrationFetch's account; user when a person is behind the call, organization for background work) picks whose, and a missing or expired one is 409 integration_not_connected with { connectPath, reconnect }; the app's declared scopes must be within the connection's sets, else integration_not_connected with reconnect: true and the missing sets; the path must be covered by a declared set (403 integration_scope_not_allowed; heldTokenCallAllowed() in packages/sdk/src/internal/oauthScopes.ts); then the token is refreshed if due and put on the call as Authorization: Bearer, the app's Authorization, cookies, X-HTTP-Method-Override and any access_token or token query key are stripped, the call is made with redirect: "manual", set-cookie is dropped from the answer, the call is counted (Usage) and logged as integration.proxy.call with the set names.
  3. Any other host: invalid_integration_route.

GET /api/integrations/<id> (the status) gains flow (app, gateway or both), connectPath, and for the caller's account connection: { account, label, scopeSets, ready, reconnect? } or null. An app shows "Connect GitHub" (a link to connectUrl()) on connection: null, "Reconnect" on reconnect: true, and calls on ready.

Scopes. As the scopes design says: a provider carries sets; an app declares the sets it needs under scopes (capabilities: { "github-oauth": { access: "read", scopes: ["identity", "repo.read"], why } }); the strict validator refuses an unknown set name for a provider that has sets and scopes on one that has none; the consent asks the union; the proxy holds the call to the declared sets. The catalog's Google and Microsoft sets are untouched: a Gateway-owned Google connection for OpenSEO asks identity, search-console.read and analytics.read, the same strings as the app-driven flow.

Who may do what

Adding, replacing a client, removing a provider, listing every connection and disconnecting someone else's account: owners and admins. Connecting and disconnecting one's own account: any member. The organization's account (account=organization): owners and admins. Rows are written by the Cloudflare login or by owners and admins through the dialog and everyapp_provider_add on a self-hosted Gateway (providers.add-is-privileged); the client is pasted by owners and admins in the dialog, or set by the login from a terminal; no tool argument ever carries a secret.

Events

provider.added and provider.removed (with provider.kind api_key or oauth), provider.updated (a client replaced, a spec replaced), provider.connected, provider.disconnected and provider.connection_expired, each with the actor and the account's kind and label, never a token (events). Held-token calls count in Usage like every proxied call; Activity stays the tools audit.

The commands

pnpm everyapp provider add stripe.json        # or an https URL; prints the hosts and the secret set to run
pnpm everyapp secret set STRIPE_API_KEY       # names the provider and its hosts, then binds the row
pnpm everyapp provider list                   # the catalog and the rows: settings set and bound, the apps listing each
pnpm everyapp provider show stripe            # the whole spec as JSON, for an agent to call it
pnpm everyapp provider remove stripe          # refuses while an active app lists it
pnpm everyapp provider presets                # the OAuth presets: GitHub, Notion, Linear, … and the built-in clients
pnpm everyapp provider add --oauth github-oauth   # registers GitHub; prints the redirect URI and the secret set to run
pnpm everyapp provider connections            # who connected what; never a token
pnpm everyapp provider disconnect github-oauth --user ben@example.com

From an AI client, the same registry is five built-ins on the Gateway connector: everyapp_provider_list for every member (with the OAuth presets), everyapp_provider_add (the spec object or { preset }, never a URL, never a replace), everyapp_provider_remove (refuses a bound or listed row), everyapp_provider_connections and everyapp_provider_disconnect (a member's own account; the organization's for owners and admins) for owners and admins on a self-hosted Gateway; see the provider tools. A key is never set through a tool.

All of them run from any directory under the Cloudflare login (as every Gateway command: --gateway, --account-id, --json, --verbose). A Gateway from before migration 0013 has no providers table: add and remove refuse and name pnpm everyapp update; list and show read it as having no rows.

Command What it does done.data under --json
pnpm everyapp provider add <spec> [--replace] <spec> is a path to a JSON file, or an https URL fetched on this machine (64 KiB at most). Parses it, runs the rules above against the catalog, the other rows and the registered apps' manifests, and writes the row. An existing id is PROVIDER_EXISTS unless --replace, which leaves the row unbound until secret set runs again; a catalog id is PROVIDER_BUILTIN. Prints what was registered, the hosts the key will go to, and the one command to run next. Run in an app's directory (where everyapp.config.ts is), it also writes .everyapp/providers/<id>.json for everyapp dev { id, name, hosts, settings, source: "row", next: [{ command: "pnpm everyapp secret set <names>", note }] }
pnpm everyapp provider list One table over the catalog's API-key, AI and OAuth entries and the rows: id, name, kind, hosts (or the host setting), each setting set or not (names only, read as secret list reads them) and for a row bound or not, where it comes from (the catalog, or registered by whom), the apps that list it. Through launch week it ends with one line, MCP apps: pnpm everyapp mcp list { providers: [{ id, name, kind, hosts, hostSetting?, settings: [{ name, set }], bound, source, addedBy?, addedAt?, apps }], mcp: "pnpm everyapp mcp list" }
pnpm everyapp provider show <id> [--save] The whole spec as JSON (providerSpecOf for an API-key provider; for an AI or OAuth entry, its name, docs, settings and hosts or token endpoints), with the hint, the smoke request, the apps that list it, and for a row who registered it, when, and whether its key is bound. --save writes .everyapp/providers/<id>.json in the current app's directory (refused outside one) { provider, source, bound, addedBy, addedAt, apps }, provider the spec
pnpm everyapp provider add --oauth <preset | built-in | spec> [--client-id <id>] [--from-env] [--replace] An OAuth provider (below): a preset (provider presets names them: github-oauth) or a spec file with auth.kind: "oauth2-user" becomes a row as provider add writes one; a built-in client (google-workspace, slack, microsoft-365, discord) writes no row. Prints the redirect URI to register at the provider's console (<gateway>/connect/<id>/callback), the scope sets, and the client's state. With --client-id and the secret on piped NAME=value lines, or --from-env (both from the variables of their setting names), the client is set in the same run through secret set, which binds the row; a client half given is PROVIDER_CLIENT_INCOMPLETE and nothing is set. Never a value on the command line. An unknown preset is PROVIDER_PRESET_UNKNOWN { id, name, kind: "oauth", source, replaced, settings, flow, redirectUri, console, scopeSets: [{ name, label, access, scopes }], client: { source: "env" | "uploaded" | "none", clientId? }, connections, next? }
pnpm everyapp provider presets The OAuth presets and the built-in clients with a Gateway flow, each with its scope sets and console. Reads nothing from the Gateway { presets: [{ id, name, builtIn, console, hosts, settings, scopeSets, hint? }] }
pnpm everyapp provider connections [--all] [--user <email>] [--provider <id>] The accounts connected to OAuth providers: the Gateway's own organization's (people's and the organization's own), or every organization's with --all; one person's with --user; one provider's with --provider. Never a token { connections: [{ id, provider, providerName, account: "user" | "organization", userId?, userEmail?, label, scopeSets, status, createdAt, lastUsedAt, expiresAt, organizationId }], organizationId? }
pnpm everyapp provider disconnect <id> --user <email> | --organization | --all Deletes the connection rows named (exactly one of the three flags: PROVIDER_CONNECTION_WHICH otherwise) and queues provider.disconnected for each. The CLI holds no token, so the grant stays at the provider until revoked there (the output names the console; Admin → Providers → Disconnect revokes it). An OAuth id with no such connection is PROVIDER_CONNECTION_NOT_FOUND; an id that isn't an OAuth provider's PROVIDER_NOT_FOUND { id, name, removed: [connection…], revoked: false, revokeAt }
pnpm everyapp provider remove <id> [--force] Removes a row; for an OAuth provider, its connections and the client uploaded under Admin → Providers go with it (the output counts them: connections, client). A catalog id is PROVIDER_BUILTIN, an id with no row PROVIDER_NOT_FOUND, and a row an active app's manifest lists is PROVIDER_IN_USE (naming the apps) unless --force. The setting stays on the Gateway; the output says secret unset removes it

provider add takes --oauth for an OAuth preset, built-in client or spec, --client-id for the client id (its secret on stdin as NAME=value, or --from-env), and --replace as for any row. source is catalog or row; addedBy is the Cloudflare login's email (or api-token:<account id>), addedAt epoch milliseconds. For an OAuth provider list and show add kind: "oauth", flow, redirectUri, client (source env, d1 or null, the client id, who, when) and the connections count. Nothing any of them prints or returns is a value: settings appear by name and set or not, a client by its id.

everyapp provider was the name of everyapp mcp until 2026-10-24. Through launch week, pnpm everyapp provider on <id> and pnpm everyapp provider off <id> stay as hidden routes that run mcp on|off and first print everyapp provider on is now everyapp mcp on; this alias goes after launch week.; provider list is this page's. The MCP apps' own codes, PROVIDER_APP_UNKNOWN and PROVIDER_KEY_UNSET, are unchanged.

Codes

What each means; the fix for a CLI code is in its data.fix and on the error codes page.

Code From When
PROVIDER_SPEC_INVALID provider add The spec doesn't parse: the message is the parser's, every problem with its path. A URL that isn't https, answers more than 64 KiB, or isn't JSON too
PROVIDER_EXISTS provider add A row has the id; --replace replaces it (and unbinds its key)
PROVIDER_BUILTIN provider add, provider remove The id is the catalog's, or everyapp
PROVIDER_NOT_FOUND provider remove, provider show Neither the catalog nor the rows have the id
PROVIDER_IN_USE provider remove An active app's manifest lists the row; the message names the apps; --force removes it anyway
PROVIDER_SECRET_TAKEN provider add A setting outside the row's prefix, or the Gateway's, the catalog's, another row's, or one a registered app lists under secrets
PROVIDER_HOST_REFUSED provider add A host that isn't public, or is the Gateway's own or under its app or third-party domain
PROVIDER_PRESET_UNKNOWN provider add --oauth No OAuth preset of that name, or a built-in that isn't an OAuth client with a Gateway flow; provider presets names them
PROVIDER_CLIENT_INCOMPLETE provider add --oauth A client id without its secret (or the reverse) was given; nothing was set, the provider is registered
PROVIDER_CONNECTION_WHICH provider disconnect None or several of --user, --organization, --all
PROVIDER_CONNECTION_NOT_FOUND provider disconnect No connection to that provider for the person or the organization named
PROVIDER_BOUND everyapp_provider_remove The row's key or client is bound; removing it is the operator's (provider remove, then secret unset)
OAUTH_PROVIDER_REQUIRED the Gateway A connect, a client upload or a connections call named a provider that isn't an OAuth provider with a flow
OAUTH_CLIENT_REQUIRED the Gateway A connect started for a provider with no live client (none set, or the pasted one unbound or unreadable); the message names the dialog and the secret set
SEALING_UNAVAILABLE the Gateway BETTER_AUTH_SECRET is unset, so nothing can be sealed or opened: no client can be pasted and no connection made until it is
PROVIDER_HOSTED the Gateway Adding a provider, pasting or forgetting a client, or removing a provider on a hosted Gateway: rows and clients are a self-hosted Gateway's (providers.add-is-privileged)
CAPABILITY_UNKNOWN deploy The manifest lists an id neither the catalog nor the Gateway's rows have; fix pnpm everyapp provider add <spec>
CAPABILITY_NOT_IN_CATALOG dev (a warning) The manifest lists an id outside the catalog: it is reached locally only through .everyapp/providers/<id>.json
invalid_integration_route the Gateway, 400 The host isn't one of the spec's (or its host setting's), the path could leave it, or the id is an AI provider's
capability_not_requested the Gateway, 403 The app's deployed manifest doesn't list the provider
integration_path_not_allowed the Gateway, 403 One of the spec's accountWidePaths
integration_method_not_allowed the Gateway, 405 A method outside readMethods, or a write method without access: "write" (the message says which)
integration_not_configured the Gateway, 503 A setting auth names is unset, or the row's key was set for other hosts (unbound); the message names the secret set to run
integration_not_available_hosted the Gateway, 503 An account provider on a hosted Gateway
integration_read_only the Gateway, 403 A write method under an agent's identity without mcp:write
integration_upstream_redirected the Gateway, 502 A query provider answered with a redirect, which is never relayed
integration_upstream_failed the Gateway, 502 The provider couldn't be reached
integration_not_connected the Gateway, 409 An OAuth provider's held-token call with no connection for the account, one that expired, or one lacking a set the app declares (missingSets); the body carries connectPath and reconnect
integration_scopes_undeclared the Gateway, 403 The app's entry for an OAuth provider with scope sets names none, or a set this Gateway doesn't offer
integration_scope_not_allowed the Gateway, 403 A held-token call to a host or path no set the app declares unlocks

The Gateway's refusals carry their code in x-everyapp-error, so gatewayRefusal() tells them from the provider's own errors.

An OAuth provider with a Gateway-owned flow (flow: "both" in its status) is connected on the Gateway host: GET /connect/<id>?account=user|organization&return=<path or app URL>&sets=a,b starts the provider's authorize flow for the signed-in member (PKCE S256, a one-use state, ten minutes), and GET /connect/<id>/callback is the redirect URI the operator registers at the provider's console (everyapp provider show <id> prints it). The connection's tokens are sealed in the Gateway's database; an app's integrationFetch() to one of the provider's api hosts carries the person's token (or the organization's, with account: "organization", which background work uses), and the app never sees it. In the Gateway's logs, provider.row_invalid names a row it left out (provider, reason the refusal code, message), once per isolate.

OAuth providers

An OAuth provider is one people connect their own account to (the design is docs/design/2026-10-oauth-providers.md); provider add takes one with --oauth: the Gateway runs the authorize flow at /connect/<id> (PKCE and state), keeps each person's (or the organization's) tokens sealed in its D1, refreshes them, and puts the access token on the app's calls through the same proxy as an API key; the app never sees the token, the refresh token or the client secret. The row's spec is the shape above with auth.kind: "oauth2-user" (authorizeUrl, tokenUrl, revokeUrl?, clientId and clientSecret as setting names, inject, pkce, scopeDialect, authorizeParams?, tokenHeaders?, identity?) and scopeSets (named bundles an app lists under scopes). The SDK ships presets of that shape (OAUTH_PRESETS: github-oauth, notion-oauth, linear-oauth, gitlab-oauth, atlassian, hubspot-oauth, zoom, dropbox, spotify, x-oauth), and the catalog's google-workspace, slack, microsoft-365 and discord clients run the same flow.

pnpm everyapp provider presets                       # the presets and built-in clients, each with its scope sets and console
pnpm everyapp provider add --oauth github-oauth      # the row, then the redirect URI to register at github.com/settings/developers
pnpm everyapp secret set GITHUB_OAUTH_CLIENT_ID GITHUB_OAUTH_CLIENT_SECRET   # the client, from the terminal (binds the row)
# or in one run, the values never on the command line:
printf 'GITHUB_OAUTH_CLIENT_SECRET=%s\n' "$SECRET" | pnpm everyapp provider add --oauth github-oauth --client-id Iv1.abc --json
pnpm everyapp provider add --oauth google-workspace  # a built-in client: no row, its client alone
pnpm everyapp provider add --oauth ./acme.json       # a custom provider by its spec

The client is a Worker secret the CLI sets (<ID>_CLIENT_ID, <ID>_CLIENT_SECRET, bound to the row's endpoints like a key to its hosts), or one pasted under Admin → Providers, which the Gateway seals in D1; a Worker secret of the same name wins, so the operator can always rotate from the terminal. provider show <id> and provider list say which is live (client.source), the redirect URI, the flow (app: the app runs the flow itself, as the social networks do; both: the Gateway runs its connect flow and still completes an app's own token calls) and how many accounts are connected. People connect at <gateway>/connect/<id> (Settings → Connected accounts, a Connect button in Admin, or an app's link from connectUrl()); provider connections lists them and provider disconnect removes one. The CLI never holds a token: disconnect deletes the row and names the console to revoke the grant at, while Admin → Providers → Disconnect revokes it from the Gateway where the preset names a revoke URL (never at Google, whose revoke is per person per client). Events: provider.added and provider.removed carry provider.kind (api_key or oauth); provider.connected, provider.disconnected and provider.connection_expired carry the account and the scope sets, never a token.

An app lists the provider as any other, with the sets it needs: capabilities: { "github-oauth": { access: "read", scopes: ["identity", "repo.read"], why } }, and calls integrationFetch({ env, provider: "github-oauth", account?: "user" | "organization" }) with the provider's own URLs; a call with no connection for the account answers 409 integration_not_connected with connectPath, and one outside the connection's sets answers it with reconnect: true.

Local development

everyapp dev has no Gateway. For a catalog provider the SDK calls the provider directly with the developer's own key from .dev.vars, under the Gateway's setting name, by the same spec. For a registered provider it needs the spec too: everyAppWorker() reads .everyapp/providers/*.json in the app's directory into EVERYAPP_DEV_PROVIDERS (a JSON array of the specs), and integrationFetch() resolves the id from it and puts the key from .dev.vars (under the spec's setting names) where the spec's auth says. The file is what pnpm everyapp provider show <id> --save writes, or provider add run in the app's directory; a spec holds no value, so the file is safe to commit. Without a file the call throws a ConfigurationError that names the command, and everyapp dev warns CAPABILITY_NOT_IN_CATALOG for the id when it starts. For an OAuth provider's held-token calls there is no flow locally: the SDK puts the developer's own token from .dev.vars on the call, under the provider's prefix plus ACCESS_TOKEN (devAccessTokenName(): GOOGLE_WORKSPACE_ACCESS_TOKEN, GITHUB_OAUTH_ACCESS_TOKEN), and integrationStatus reports a connection while it is set; without it the call answers integration_not_connected as the Gateway would.

The catalog

Generated from CAPABILITIES by pnpm -C packages/cli docs:update; a test fails CI when it is stale. Step by step for each: the everyapp skill's setup/providers.md, under the provider's id (setup/providers.md#<id>), and the provider's own setup file where it has one.

The built-in catalog has 74 providers: 47 API-key, 9 AI, 18 OAuth clients.

Id Name Kind Settings Hosts How the key is sent Where the key comes from Smoke request Verified
dataforseo DataForSEO api_key DATAFORSEO_API_KEY api.dataforseo.com, sandbox.dataforseo.com Authorization: Basic <DATAFORSEO_API_KEY>, the value login:password base64-encoded (or sent as it is when it holds no colon: already encoded) https://app.dataforseo.com/api-access; API reference https://docs.dataforseo.com/v3/ GET https://api.dataforseo.com/v3/appendix/user_data, expecting 200; free. Account-wide, so the Gateway refuses it for apps; run it from the person's terminal. 2026-10-25, live
context Context.dev api_key CONTEXT_API_KEY api.context.dev Authorization: Bearer <CONTEXT_API_KEY> https://context.dev/ none recorded 2026-10-25, live
stripe Stripe api_key STRIPE_API_KEY api.stripe.com, files.stripe.com Authorization: Bearer <STRIPE_API_KEY> https://dashboard.stripe.com/apikeys; API reference https://docs.stripe.com/api GET https://api.stripe.com/v1/balance, expecting 200; free. A restricted key needs Balance read for this one. 2026-10-25, docs
lemonsqueezy Lemon Squeezy api_key LEMONSQUEEZY_API_KEY api.lemonsqueezy.com Authorization: Bearer <LEMONSQUEEZY_API_KEY> https://app.lemonsqueezy.com/settings/api; API reference https://docs.lemonsqueezy.com/api GET https://api.lemonsqueezy.com/v1/users/me, expecting 200; free. Account-wide, so the Gateway refuses it for apps; run it from the person's terminal. 2026-10-25, docs
paddle Paddle api_key PADDLE_API_KEY api.paddle.com, sandbox-api.paddle.com Authorization: Bearer <PADDLE_API_KEY> https://developer.paddle.com/api-reference/about/authentication/; API reference https://developer.paddle.com/api-reference/ GET https://api.paddle.com/event-types, expecting 200; free. Paddle's own first request: needs no permissions. Send it to sandbox-api.paddle.com with a sandbox key. 2026-10-25, docs
shopify Shopify api_key SHOPIFY_API_TOKEN, SHOPIFY_HOST the host <SHOPIFY_HOST> names (a bare host or an https origin) X-Shopify-Access-Token: <SHOPIFY_API_TOKEN> https://shopify.dev/docs/apps/build/authentication-authorization/legacy/admin-custom-apps; API reference https://shopify.dev/docs/api/admin-graphql GET https://<SHOPIFY_HOST>/admin/api/2026-10/shop.json, expecting 200; free. The legacy REST read of the store; the Gateway forwards no GET for this entry, so run it from the person's terminal. 2026-10-25, docs
plaid Plaid api_key PLAID_SECRET sandbox.plaid.com, production.plaid.com PLAID-SECRET: <PLAID_SECRET> https://dashboard.plaid.com/developers/keys; API reference https://plaid.com/docs/api/ none recorded no: not checked against the provider's docs
attio Attio api_key ATTIO_API_KEY api.attio.com Authorization: Bearer <ATTIO_API_KEY> https://attio.com/help/reference/apps/generating-an-api-key; API reference https://docs.attio.com/rest-api/overview GET https://api.attio.com/v2/objects, expecting 200; free. Lists the workspace's objects; needs the object_configuration:read scope. /v2/self answers 200 even for a revoked token. 2026-10-25, docs
hubspot HubSpot api_key HUBSPOT_API_TOKEN api.hubapi.com Authorization: Bearer <HUBSPOT_API_TOKEN> https://developers.hubspot.com/docs/apps/legacy-apps/private-apps/overview; API reference https://developers.hubspot.com/docs/api-reference/latest/overview GET https://api.hubapi.com/account-info/2026-09/details, expecting 200; free. Account-wide, so the Gateway refuses it for apps; run it from the person's terminal. 2026-10-25, docs
pipedrive Pipedrive api_key PIPEDRIVE_API_TOKEN api.pipedrive.com x-api-token: <PIPEDRIVE_API_TOKEN> https://app.pipedrive.com/settings/api; API reference https://developers.pipedrive.com/docs/api/v1 GET https://api.pipedrive.com/v1/users/me, expecting 200; free. Account-wide, so the Gateway refuses it for apps; run it from the person's terminal. 2026-10-25, docs
airtable Airtable api_key AIRTABLE_API_TOKEN api.airtable.com Authorization: Bearer <AIRTABLE_API_TOKEN> https://airtable.com/create/tokens; API reference https://airtable.com/developers/web/api/introduction GET https://api.airtable.com/v0/meta/whoami, expecting 200; free. Account-wide, so the Gateway refuses it for apps; run it from the person's terminal. 2026-10-25, docs
notion Notion api_key NOTION_API_TOKEN api.notion.com Authorization: Bearer <NOTION_API_TOKEN> https://app.notion.com/developers/connections; API reference https://developers.notion.com/reference/intro GET https://api.notion.com/v1/users/me, expecting 200; free. Send Notion-Version: 2026-03-11 with it; account-wide, so run it from the person's terminal. 2026-10-25, docs
postmark Postmark api_key POSTMARK_API_TOKEN api.postmarkapp.com X-Postmark-Server-Token: <POSTMARK_API_TOKEN> https://postmarkapp.com/support/article/1008-what-are-the-account-and-server-api-tokens; API reference https://postmarkapp.com/developer/api/overview GET https://api.postmarkapp.com/server, expecting 200; free. Returns the server's settings; account-wide, so run it from a terminal. 2026-10-25, docs
sendgrid SendGrid api_key SENDGRID_API_KEY api.sendgrid.com, api.eu.sendgrid.com Authorization: Bearer <SENDGRID_API_KEY> https://app.sendgrid.com/settings/api_keys; API reference https://www.twilio.com/docs/sendgrid/api-reference GET https://api.sendgrid.com/v3/scopes, expecting 200; free. Lists the key's own scopes (the docs don't say whether a Mail-Send-only key may call it); account-wide, so run it from a terminal. 2026-10-25, docs
twilio Twilio api_key TWILIO_API_SECRET, TWILIO_API_KEY api.twilio.com Authorization: Basic base64(<TWILIO_API_KEY>:<TWILIO_API_SECRET>) https://www.twilio.com/docs/iam/api-keys/keys-in-console; API reference https://www.twilio.com/docs/usage/requests-to-twilio none recorded 2026-10-25, docs
intercom Intercom api_key INTERCOM_API_TOKEN api.intercom.io, api.eu.intercom.io, api.au.intercom.io Authorization: Bearer <INTERCOM_API_TOKEN> https://app.intercom.com/a/apps/_/developer-hub; API reference https://developers.intercom.com/docs/build-an-integration/learn-more/rest-apis GET https://api.intercom.io/me, expecting 200; free. Returns the admin behind the token; account-wide, so run it from a terminal. 2026-10-25, docs
crisp Crisp api_key CRISP_API_KEY, CRISP_API_IDENTIFIER api.crisp.chat Authorization: Basic base64(<CRISP_API_IDENTIFIER>:<CRISP_API_KEY>) https://docs.crisp.chat/guides/rest-api/authentication/website-token/; API reference https://docs.crisp.chat/references/rest-api/v1/ none recorded 2026-10-25, docs
linear Linear api_key LINEAR_API_KEY api.linear.app Authorization: <LINEAR_API_KEY> https://linear.app/settings/account/security; API reference https://linear.app/developers/graphql none recorded 2026-10-25, docs
github GitHub api_key GITHUB_API_TOKEN api.github.com Authorization: Bearer <GITHUB_API_TOKEN> https://github.com/settings/personal-access-tokens/new; API reference https://docs.github.com/en/rest GET https://api.github.com/user, expecting 200; free. Returns the token's owner; 401 means the token is wrong. 2026-10-25, docs
gitlab GitLab api_key GITLAB_API_TOKEN gitlab.com PRIVATE-TOKEN: <GITLAB_API_TOKEN> https://gitlab.com/-/user_settings/personal_access_tokens; API reference https://docs.gitlab.com/api/rest/ GET https://gitlab.com/api/v4/personal_access_tokens/self, expecting 200; free. Describes a personal access token itself (401 when it is wrong or revoked); account-wide, so run it from a terminal. 2026-10-25, docs
vercel Vercel api_key VERCEL_API_TOKEN api.vercel.com Authorization: Bearer <VERCEL_API_TOKEN> https://vercel.com/account/tokens; API reference https://vercel.com/docs/rest-api GET https://api.vercel.com/v10/projects, expecting 200; free. Lists the projects the token can reach. 2026-10-25, docs
cloudflare Cloudflare api_key CLOUDFLARE_PROVIDER_API_TOKEN api.cloudflare.com Authorization: Bearer <CLOUDFLARE_PROVIDER_API_TOKEN> https://dash.cloudflare.com/profile/api-tokens; API reference https://developers.cloudflare.com/api/ GET https://api.cloudflare.com/client/v4/user/tokens/verify, expecting 200; free. For a user token; account-owned tokens use /client/v4/accounts//tokens/verify. Account-wide, so run it from the person's terminal. 2026-10-25, docs
posthog PostHog api_key POSTHOG_PERSONAL_API_KEY us.posthog.com, eu.posthog.com Authorization: Bearer <POSTHOG_PERSONAL_API_KEY> https://us.posthog.com/settings/user-api-keys; API reference https://posthog.com/docs/api GET https://us.posthog.com/api/users/@me/, expecting 200; free. Needs the user:read scope; use eu.posthog.com for an EU project. Account-wide, so run it from the person's terminal. 2026-10-25, docs
mixpanel Mixpanel api_key MIXPANEL_SERVICE_ACCOUNT_SECRET, MIXPANEL_SERVICE_ACCOUNT_USERNAME 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 Authorization: Basic base64(<MIXPANEL_SERVICE_ACCOUNT_USERNAME>:<MIXPANEL_SERVICE_ACCOUNT_SECRET>) https://mixpanel.com/settings/org#serviceaccounts; API reference https://docs.mixpanel.com/reference/overview GET https://mixpanel.com/api/app/me, expecting 200; free. Lists the organizations and projects the service account reaches. Account-wide, so run it from the person's terminal. 2026-10-25, docs
segment Segment api_key SEGMENT_WRITE_KEY api.segment.io, events.eu1.segmentapis.com Authorization: Basic base64(<SEGMENT_WRITE_KEY>:) https://www.twilio.com/docs/segment/connections/find-writekey; API reference https://www.twilio.com/docs/segment/connections/sources/catalog/libraries/server/http-api none recorded 2026-10-25, docs
supabase Supabase api_key SUPABASE_SECRET_KEY, SUPABASE_HOST the host <SUPABASE_HOST> names (a bare host or an https origin) apikey: <SUPABASE_SECRET_KEY> https://supabase.com/dashboard/project/_/settings/api-keys; API reference https://supabase.com/docs/guides/api GET https://<SUPABASE_HOST>/rest/v1/, expecting 200; free. The REST API's OpenAPI root, which only a secret key may read; sent to the host in SUPABASE_HOST. 2026-10-25, docs
turso Turso api_key TURSO_API_TOKEN api.turso.tech Authorization: Bearer <TURSO_API_TOKEN> https://docs.turso.tech/api-reference/authentication; API reference https://docs.turso.tech/api-reference/introduction GET https://api.turso.tech/v1/auth/validate, expecting 200; free. Account-wide, so run it from the person's terminal. 2026-10-25, docs
turso-db Turso database api_key TURSO_DB_AUTH_TOKEN, TURSO_DB_HOST the host <TURSO_DB_HOST> names (a bare host or an https origin) Authorization: Bearer <TURSO_DB_AUTH_TOKEN> https://docs.turso.tech/sdk/authorization/tokens; API reference https://docs.turso.tech/sdk/http/reference none recorded 2026-10-25, docs
neon Neon api_key NEON_API_KEY console.neon.tech Authorization: Bearer <NEON_API_KEY> https://neon.com/docs/manage/api-keys; API reference https://neon.com/docs/reference/api GET https://console.neon.tech/api/v2/auth, expecting 200; free. Says which credential and identity the key is. Account-wide, so run it from the person's terminal. 2026-10-25, docs
pinecone Pinecone api_key PINECONE_API_KEY, PINECONE_HOST api.pinecone.io, the host <PINECONE_HOST> names (a bare host or an https origin) Api-Key: <PINECONE_API_KEY> https://app.pinecone.io/organizations/-/projects/-/keys; API reference https://docs.pinecone.io/reference/api/introduction GET https://api.pinecone.io/indexes, expecting 200; free. Lists the project's indexes; the app sends X-Pinecone-Api-Version itself. 2026-10-25, docs
mapbox Mapbox api_key MAPBOX_ACCESS_TOKEN api.mapbox.com ?access_token=<MAPBOX_ACCESS_TOKEN> on the URL, in place of any the app sent https://console.mapbox.com/account/access-tokens/; API reference https://docs.mapbox.com/api/overview/ none recorded 2026-10-25, docs
openweather OpenWeather api_key OPENWEATHER_API_KEY api.openweathermap.org, pro.openweathermap.org, history.openweathermap.org, tile.openweathermap.org, maps.openweathermap.org ?appid=<OPENWEATHER_API_KEY> on the URL, in place of any the app sent https://home.openweathermap.org/api_keys; API reference https://openweathermap.org/api GET https://api.openweathermap.org/data/2.5/weather?lat=51.51&lon=-0.13, expecting 200; free. Counts one call against the plan's quota; free within it. A bad or not yet active key answers 401. 2026-10-25, docs
voyage Voyage AI api_key VOYAGE_API_KEY api.voyageai.com Authorization: Bearer <VOYAGE_API_KEY> https://dashboard.voyageai.com/organization/api-keys; API reference https://docs.voyageai.com/reference/embeddings-api GET https://api.voyageai.com/v1/batches, expecting 200; free. Lists the organization's batches, so the Gateway refuses it for apps; run it from the person's terminal. 2026-10-25, docs
perplexity Perplexity api_key PERPLEXITY_API_KEY api.perplexity.ai Authorization: Bearer <PERPLEXITY_API_KEY> https://console.perplexity.ai/project/keys; API reference https://docs.perplexity.ai/api-reference/agent-post GET https://api.perplexity.ai/orchestrator/v1/models, expecting 200; free 2026-10-25, docs
jina Jina AI api_key JINA_API_KEY api.jina.ai, r.jina.ai, s.jina.ai Authorization: Bearer <JINA_API_KEY> https://jina.ai/api-dashboard/key-manager; API reference https://docs.jina.ai/ GET https://api.jina.ai/v1/batches, expecting 200; free. Lists the account's batch jobs, so the Gateway refuses it for apps; run it from the person's terminal. 2026-10-25, docs
replicate Replicate api_key REPLICATE_API_TOKEN api.replicate.com Authorization: Bearer <REPLICATE_API_TOKEN> https://replicate.com/account/api-tokens; API reference https://replicate.com/docs/reference/http GET https://api.replicate.com/v1/account, expecting 200; free. Account-wide, so the Gateway refuses it for apps; run it from the person's terminal. 2026-10-25, docs
huggingface Hugging Face Inference api_key HUGGINGFACE_API_TOKEN router.huggingface.co Authorization: Bearer <HUGGINGFACE_API_TOKEN> https://huggingface.co/settings/tokens/new?ownUserPermissions=inference.serverless.write&tokenType=fineGrained; API reference https://huggingface.co/docs/inference-providers/index none recorded 2026-10-25, docs
firecrawl Firecrawl api_key FIRECRAWL_API_KEY api.firecrawl.dev Authorization: Bearer <FIRECRAWL_API_KEY> https://www.firecrawl.dev/app/api-keys; API reference https://docs.firecrawl.dev/api-reference/introduction GET https://api.firecrawl.dev/v2/team/credit-usage, expecting 200; free. Remaining credits; account-wide, so the Gateway refuses it for apps: run it from the person's terminal. 2026-10-25, docs
exa Exa api_key EXA_API_KEY api.exa.ai x-api-key: <EXA_API_KEY> https://dashboard.exa.ai/api-keys; API reference https://exa.ai/docs/reference/search GET https://api.exa.ai/websets/v0/teams/me, expecting 200; free. Team info under the Websets API (the docs page leaves out the /websets prefix); account-wide, so run it from the person's terminal. 2026-10-25, docs
tavily Tavily api_key TAVILY_API_KEY api.tavily.com Authorization: Bearer <TAVILY_API_KEY> https://app.tavily.com; API reference https://docs.tavily.com/documentation/api-reference/endpoint/search GET https://api.tavily.com/usage, expecting 200; free. Key and account usage, not an endpoint that costs credits; account-wide, so run it from the person's terminal (10 calls per 10 minutes). 2026-10-25, docs
serper Serper api_key SERPER_API_KEY google.serper.dev, scrape.serper.dev X-API-KEY: <SERPER_API_KEY> https://serper.dev/api-keys; API reference https://serper.dev/playground none recorded no: not checked against the provider's docs
brave-search Brave Search api_key BRAVE_SEARCH_API_KEY api.search.brave.com X-Subscription-Token: <BRAVE_SEARCH_API_KEY> https://api-dashboard.search.brave.com/app/keys; API reference https://api-dashboard.search.brave.com/documentation none recorded 2026-10-25, docs
apify Apify api_key APIFY_API_TOKEN api.apify.com Authorization: Bearer <APIFY_API_TOKEN> https://console.apify.com/settings/integrations; API reference https://docs.apify.com/api/v2 GET https://api.apify.com/v2/users/me, expecting 200; free. The docs' verify call; account-wide, so the Gateway refuses it for apps: run it from the person's terminal. 2026-10-25, docs
deepgram Deepgram api_key DEEPGRAM_API_KEY api.deepgram.com, api.eu.deepgram.com, api.au.deepgram.com, api.in.deepgram.com Authorization: Token <DEEPGRAM_API_KEY> https://console.deepgram.com/; API reference https://developers.deepgram.com/reference/deepgram-api-overview GET https://api.deepgram.com/v1/models, expecting 200; free. Lists the public models; it needs the key and processes no audio. 2026-10-25, docs
elevenlabs ElevenLabs api_key ELEVENLABS_API_KEY api.elevenlabs.io xi-api-key: <ELEVENLABS_API_KEY> https://elevenlabs.io/app/settings/api-keys; API reference https://elevenlabs.io/docs/api-reference/introduction GET https://api.elevenlabs.io/v1/models, expecting 200; free. The first request in ElevenLabs' authentication docs; it generates nothing. 2026-10-25, docs
assemblyai AssemblyAI api_key ASSEMBLYAI_API_KEY api.assemblyai.com, api.eu.assemblyai.com Authorization: <ASSEMBLYAI_API_KEY> https://www.assemblyai.com/dashboard/home; API reference https://www.assemblyai.com/docs GET https://api.assemblyai.com/v2/transcript?limit=1, expecting 200; free. Lists the account's latest transcript; it transcribes nothing. 2026-10-25, docs
newsapi NewsAPI api_key NEWSAPI_API_KEY newsapi.org X-Api-Key: <NEWSAPI_API_KEY> https://newsapi.org/register; API reference https://newsapi.org/docs none recorded 2026-10-25, docs
openai OpenAI ai OPENAI_API_KEY api.openai.com Authorization: Bearer <OPENAI_API_KEY> https://platform.openai.com/api-keys; API reference https://platform.openai.com/docs/api-reference none recorded 2026-10-25, live
openrouter OpenRouter ai OPENROUTER_API_KEY openrouter.ai Authorization: Bearer <OPENROUTER_API_KEY> https://openrouter.ai/settings/keys; API reference https://openrouter.ai/docs none recorded 2026-10-25, live
workers-ai Workers AI ai WORKERS_AI_API_TOKEN, CLOUDFLARE_ACCOUNT_ID (set by everyapp setup) api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai Authorization: Bearer <WORKERS_AI_API_TOKEN> https://dash.cloudflare.com/profile/api-tokens; API reference https://developers.cloudflare.com/workers-ai/ none recorded 2026-10-25, live
ai-gateway Cloudflare AI Gateway ai AI_GATEWAY_API_KEY, CLOUDFLARE_ACCOUNT_ID (set by everyapp setup), AI_GATEWAY_ID gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{AI_GATEWAY_ID} cf-aig-authorization: Bearer <AI_GATEWAY_API_KEY> https://dash.cloudflare.com/?to=/:account/ai/ai-gateway; API reference https://developers.cloudflare.com/ai-gateway/ none recorded 2026-10-25, docs
anthropic Anthropic ai ANTHROPIC_API_KEY api.anthropic.com Authorization: Bearer <ANTHROPIC_API_KEY> https://platform.claude.com/settings/keys; API reference https://platform.claude.com/docs/en/api/overview GET https://api.anthropic.com/v1/models, expecting 200; free. Send an anthropic-version header, as every call does. 2026-10-25, docs
gemini Google Gemini ai GEMINI_API_KEY generativelanguage.googleapis.com Authorization: Bearer <GEMINI_API_KEY> https://aistudio.google.com/apikey; API reference https://ai.google.dev/gemini-api/docs/openai GET https://generativelanguage.googleapis.com/v1beta/openai/models, expecting 200; free 2026-10-25, docs
mistral Mistral AI ai MISTRAL_API_KEY api.mistral.ai Authorization: Bearer <MISTRAL_API_KEY> https://console.mistral.ai; API reference https://docs.mistral.ai/api/ GET https://api.mistral.ai/v1/models, expecting 200; free 2026-10-25, docs
groq Groq ai GROQ_API_KEY api.groq.com Authorization: Bearer <GROQ_API_KEY> https://console.groq.com/keys; API reference https://console.groq.com/docs/api-reference GET https://api.groq.com/openai/v1/models, expecting 200; free 2026-10-25, docs
cohere Cohere ai COHERE_API_KEY api.cohere.com Authorization: Bearer <COHERE_API_KEY> https://dashboard.cohere.com/api-keys; API reference https://docs.cohere.com/reference/about GET https://api.cohere.com/v1/models, expecting 200; free 2026-10-25, docs

OAuth clients (oauth_client: the Gateway completes their token endpoints only, and the spec parser takes none as a row): linkedin, facebook, instagram, threads, youtube, tiktok, tiktok-business, reddit, pinterest, discord, slack, mastodon, kick, twitch, tumblr, dribbble, google-workspace, microsoft-365. Their settings, consoles, redirect URIs, token endpoints and scope sets are generated in the capabilities document. Gateway services (service: no host, no spec): email, members.

Sources

  • The catalog, the auth kinds and CapabilityLookup: packages/sdk/src/internal/capabilities.ts.
  • The spec parser: packages/sdk/src/internal/providerSpec.ts. The row rules and the registry: packages/sdk/src/internal/providerRegistry.ts.
  • The Gateway's registry and its host rule: apps/every-app-gateway/src/server/providers/index.ts; the table: apps/every-app-gateway/src/db/providers.schema.ts; the reads: apps/every-app-gateway/src/server/repositories/ProviderRepository.ts.
  • The proxy and the order of its checks: apps/every-app-gateway/src/server/integration-proxy.ts, written out in the capabilities document.
  • The commands: packages/cli/src/commands/provider/.
  • The design and its decisions: docs/design/2026-10-provider-registry.md in the repository. The invariants: the threat model, B4.

On this page