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 statusconfigured: false, and every app that lists it shows "Needs setup" (its setting reads as unset instatus,secret listand 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 untilsecret setruns again: calls answer503 integration_not_configuredwith<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 removeleaves the setting on the Gateway Worker (pnpm everyapp secret unset STRIPE_API_KEYremoves it), and nothing reads it until a new row is bound by a newsecret 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'sclientId, 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,slackanddiscordoffer 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:
- 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_IDand 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. - The
oauth_clientstable, 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 asv1.<nonce>.<ciphertext>; the key derived once per isolate with HKDF-SHA256 fromBETTER_AUTH_SECRETwith the infoeveryapp: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 accountsAn 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:
- The host is one of the provider's token endpoints: the app-driven branch, unchanged (OAuth clients).
- 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 onaccess: "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'saccount;userwhen a person is behind the call,organizationfor background work) picks whose, and a missing or expired one is409 integration_not_connectedwith{ connectPath, reconnect }; the app's declaredscopesmust be within the connection's sets, elseintegration_not_connectedwithreconnect: trueand the missing sets; the path must be covered by a declared set (403 integration_scope_not_allowed;heldTokenCallAllowed()inpackages/sdk/src/internal/oauthScopes.ts); then the token is refreshed if due and put on the call asAuthorization: Bearer, the app'sAuthorization, cookies,X-HTTP-Method-Overrideand anyaccess_tokenortokenquery key are stripped, the call is made withredirect: "manual",set-cookieis dropped from the answer, the call is counted (Usage) and logged asintegration.proxy.callwith the set names. - 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.comFrom 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 specThe 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/ |
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.
The deploy pipeline
What pnpm everyapp deploy does from its first line to Live, in order, with every check that can stop it, what the app policy refuses, every flag, release installs (install), release, the Gateway's own deploy API and browser installs, delete, and what a redeploy keeps.
AI: the default model provider and AI Gateway
How an app gets a chat model without naming a provider: the manifest's ai.chat, the SDK's ai(), the /api/ai/default route, the order the Gateway resolves the default in (OpenRouter, Workers AI, OpenAI, Anthropic, Groq, Mistral, Gemini), everyapp ai default|show|list|gateway with their --json data and the MCP twins, Cloudflare AI Gateway in front of every provider, what each AI entry's fields mean, the usage by model, the ai.default_changed event, and why OpenRouter is the first default and AI Gateway is never one.