Driving setup for a person
The procedure an agent follows when a person says "set up EveryApp for me" or "deploy this" — the commands in order, the exact points where it must stop and hand off to the person and what to say, how to resume with the same command, and how to verify with status --json.
This is the agent's side of the journey the user docs describe in /docs/gateway, /docs/create-an-app, /docs/update and /docs/people. Its short form is the everyapp skill, packages/cli/skills/everyapp/SKILL.md, which create copies into every new app; this page follows the skill's rules and spells out what each command emits and where it stops. Step-by-step setup of each integration (the Gateway, Access, email, the AI providers, Google, Microsoft 365, Slack) is in packages/cli/skills/everyapp/setup/ (index README.md), copied into every app as .claude/skills/everyapp/setup/ and .agents/skills/everyapp/setup/; the matching file is named at each step below. The prompt a person pastes into their agent is in /docs/gateway.
Every command below is pnpm everyapp …, run from the EveryApp checkout (publishing to npm is paused; npx everyapp runs an old CLI that can't deploy this Gateway). Agents always add --json.
How long setup takes, and why the timeout matters. A fresh setup runs 5 to 20 minutes end to end. The Gateway build from the checkout (usually 1 to 2 minutes), the upload and the new hostname's certificate (gateway.tls, up to 10 minutes, DEFAULT_MAX_WAIT_MS in packages/cli/src/lib/domains/certWait.ts) take 5 to 10 together, which the setup.gateway start event says in data.expectedMinutes: { min: 5, max: 10 }; then comes the owner link, which the run polls every 5 seconds for up to 15 minutes (OWNER_WAIT_MS in packages/cli/src/lib/gateway/setup-link.ts). Give the command a long timeout, or run it in the background and keep reading its events; a tool that stops a command after a few minutes (Claude Code's shell moves one to the background after 120 seconds and ends it at 10 minutes when run with -p) loses the owner link mid-wait. Nothing is lost for good: run data.rerun as it is (pnpm everyapp setup --json: --json included because this run had it) and setup resumes, minting a new link and retiring the old one.
What only a person can do
Each row is a point where the run stops (or waits) and you hand off. Say what to click and what to come back with, in a sentence or two. Field names in backticks are what you substitute from the event.
| Handoff | Signal | What to say |
|---|---|---|
| Create the Cloudflare account | None; ask before setup when the person has no account. |
"Sign up at https://dash.cloudflare.com/sign-up and verify your email. Tell me when you're in." |
| Pay for Workers Paid | action_required upgrade_workers_paid (exit 2), data.url; or warning WORKERS_PAID_UNVERIFIED (the run goes on). |
"EveryApp needs Workers Paid ($5 a month, then usage). Open data.url, choose the Paid plan and enter a card. Tell me when it's done and I'll run setup again." |
| Buy or onboard the domain | action_required add_domain (exit 2), data.url; or error ZONE_NOT_FOUND with data.zones. |
No domain: "EveryApp needs a domain in this Cloudflare account. Buy one at data.url (about $10 a year for a .com, ready in minutes), or point a domain you own at Cloudflare's nameservers. Which name should the Gateway use, e.g. apps.yourcompany.com?" Pending zone: "Finish the nameserver step for data.zone at data.url and wait for Cloudflare's email saying it's active." |
| Approve the Cloudflare login | action_required cloudflare_login, data.waiting: true, data.url, data.code, data.expiresAt (5 minutes). |
"Open data.url and enter the code data.code. I'm waiting; it's good for 5 minutes." |
| Choose the Cloudflare account or Gateway | action_required choose_account (exit 2), data.choices[] each { accountId, accountName, label, rerun }, plus gatewayUrl (https://<host> or null) when the choice is among existing Gateways (label is then <account> · <host>, or the account name alone when the rerun names the Gateway by --domain <host>); data.rerun is null. |
"Your login can use several Cloudflare accounts: label for each. Which one?" Then run that choice's rerun as it is: it carries --json when this run had it. |
| Pick the workers.dev subdomain | action_required register_workers_subdomain (exit 2), data.url. |
"Cloudflare asks every account to pick a workers.dev name once before its first Worker; your apps never use it. Open data.url, pick any name, and tell me when it's done." |
| Open the owner link | action_required claim_owner, data.waiting: true, data.url, data.expiresAt, data.replaced. |
"Open this link in your browser: data.url. It opens a page called Set up your gateway: enter your name, email and a password of at least 8 characters, then press Create owner account. The link works once and expires in about 24 hours; whoever opens it first becomes the owner, so don't share it. I'm waiting." If the run stops first (exit 2 after 15 minutes), run data.rerun as given once they say done; it carries --json. |
| Confirm at a terminal | action_required rerun_interactive (exit 2) with data.rerun, data.rerunNote ("in a terminal", or "in a terminal, and confirm cf's prompt" when cf refused the upload) and data.needsTerminal: true; or warning OWNER_NOT_CLAIMED (setup under CI), also with data.needsTerminal: true. |
"The Worker was changed outside everyapp, so Cloudflare won't replace it unattended. Run data.rerun in your own terminal and answer its prompt, then tell me." Hand the command over exactly as given, without --json: with it added back the run stops at the same rerun_interactive again. |
| Enter the browser-install API token | Interactive setup only: "Configure browser app installs?" then a hidden "Cloudflare API token" prompt. Never asked under --json. |
Only if the person wants installs from the browser: "Run pnpm everyapp setup yourself at a terminal and answer Yes to Configure browser app installs, then paste the token when asked." Never ask for the token in chat. |
| Put Cloudflare Access in front | On by default once the owner exists. At a terminal: "Put Cloudflare Access in front of your Gateway?" (Yes by default). Unattended (--json, --yes, CI) it goes on without a question when the account can (a Zero Trust team exists, and the Cloudflare login is an owner or admin of the Gateway); otherwise one warning ACCESS_SETUP_SKIPPED says why and names pnpm everyapp access on (below). --no-access leaves it off. |
When the warning names a team to pick: the team name is the person's to pick (permanent), then access on --team <name> --json. When the login isn't an owner or admin: say so; access on at a terminal, or once they are. |
| Pick the Zero Trust team name (Access) | Error ACCESS_TEAM_REQUIRED, data.suggested (from access on, below). |
"Cloudflare Access needs a team name. People sign in at that name followed by .cloudflareaccess.com, and it can't be changed later. I suggest data.suggested; which name?" |
| Provide a secret's value | Error SECRET_VALUE_REQUIRED from configure <app> under --json with nothing piped, or with --from-env (which never reads stdin) and no variable of a missing name found (data.names says which), or from secret set under --json without --from-env. |
"Run pnpm everyapp configure crm yourself: it asks for each key and says where to get it. Or export each key under its own name (OPENAI_API_KEY, …) in the shell I run in and I'll run it with --from-env. Don't paste a key here." |
A deploy needs none of the setup handoffs except the login, choose_account, rerun_interactive, install_gateway (the person's yes to installing a Gateway) and the takeover flags (data.authorize: true): see Deploy.
Prerequisites
Check these before the first command, and say what is missing in one line each.
| Check | Command and expected result |
|---|---|
| Node.js | node --version is v22.18.0 or newer (MIN_NODE_VERSION in packages/cli/src/lib/runtime.ts). Older: error UNSUPPORTED_RUNTIME "everyapp needs Node.js 22.18.0 or newer; this is …", fix "install Node 22 or 24, then run the command again". Bun is refused with the same code: "everyapp runs on Node.js, not Bun". |
| pnpm | pnpm --version. The root package.json pins "packageManager": "pnpm@10.34.5"; a newer pnpm switches to it by itself. Missing: npm install -g pnpm. |
| git | git --version; needed for the clone and for git pull later. |
| The checkout | git clone https://github.com/every-app/every-app.git and run everything inside that directory. everyapp finds its checkout from where its own code lives (findCheckoutRoot walks up from the CLI module, packages/cli/src/lib/checkout.ts), not from the working directory: CHECKOUT_MISSING ("everyapp has to run from a checkout of the EveryApp repository. This copy is installed in …, outside any checkout.", fix "in your checkout, run pnpm everyapp …") means the running copy was installed outside any checkout (npx everyapp); run pnpm everyapp from the cloned repository instead. |
| Dependencies | pnpm install in the checkout. It also builds the SDK into packages/sdk/dist, which the apps and the Gateway load. |
| The CLI | pnpm everyapp --version prints everyapp and its version (0.3.0 in packages/cli/package.json at the time of writing). The launcher packages/cli/bin/everyapp.js rebuilds the CLI when its source or the SDK's changed, so the first run takes a few seconds. |
| Cloudflare's CLI | Nothing to install: cf is pinned and run by everyapp. setup reports it in its first step, setup.toolchain (data.node, data.cf). |
The person also needs a Cloudflare account on Workers Paid and a domain on Cloudflare; both are handoffs above, and setup checks them in order.
How a --json run talks to you
From packages/cli/src/lib/output/events.ts and packages/cli/src/lib/errors.ts:
- stdout carries one JSON object per line, each
{ "v": 1, "ts": "…", "type": "…", … };typeisstep,progress,warning,action_required,errorordone. Nothing is ever prompted under--json. - Exit codes: 0 after
done; 1 after anerror, whatever came before it; 2 when the run stopped on anaction_required; 251 for a command it can't find (errorUNKNOWN_COMMAND) and 252 for bad arguments (UNKNOWN_FLAG,INVALID_VALUE,MISSING_ARGUMENT, …), whosedatacarrieshelp(the--helpcommand to run),usageordidYouMean(packages/cli/src/argumentErrors.ts). action_requiredhasaction(one ofcloudflare_login,register_workers_subdomain,claim_owner,upgrade_workers_paid,rerun_interactive,add_domain,choose_account,install_gateway),message(headline on the first line, detail after),data.url,data.code(device login),data.expiresAt,data.waiting,data.rerunanddata.rerunNote, ordata.choices[](each withrerunandlabel) when the way on is one of several commands.data.waiting: truemeans the run waits for the person (a login to approve, the owner link): showurlandcode, keep reading. If the person acts in time the run goes on todone(exit 0) or anerror(exit 1); if not, a lastaction_requiredends it (exit 2).errorhas a stablecode,retryable,data.fix(a remedy in words),data.rerunwithdata.rerunNote, anddata.authorize: truewhen the rerun carries a flag that takes something over (--force-dns,--allow-delete-durable-object-data,--allow-delete-workflows,--enable-email-routing,--yes,--allow-downgrade): show the error and ask before running it, never retry with it on your own.code: "CANCELLED"is a person declining a prompt.data.rerunis the same command,--jsonincluded when this run had it (thisCommandinpackages/cli/src/lib/output/command.ts): run it as it is. The exception isdata.needsTerminal: true(ActionDatainevents.ts: "Only a person at a terminal can do the rerun"), which bothrerun_interactiveand theOWNER_NOT_CLAIMEDwarning carry; those reruns are built without--json(thisCommand([], { terminal: true })), and theirdata.rerunNoteis "in a terminal" ("in a terminal, and confirm cf's prompt" when cf refused the upload) or "at a terminal" for the warning. Hand those commands over as they are, never with--json; with it added the run stops at the same point again. Every suggested command starts withpnpm everyappbecause pnpm started the CLI.- A warning is emitted once per run;
donecarriesdata.durationMs, and for setup and deploydata.factsanddata.next. - "Interactive" means
canPromptInTerminal(packages/cli/src/lib/output/prompt.ts): stdin and stderr are TTYs, no--json, and noCIenvironment variable; piping stdout changes nothing. Without--jsonand without a terminal on stdin and stderr (an agent's shell, CI), setup stops atcloudflare_loginwith no device code ("Log in to Cloudflare with pnpm everyapp login. Unattended? Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID instead."): only--jsonhands cf's device link and code over (onDeviceLoginis wired whenflags.jsoninpackages/cli/src/commands/setup/runSetup.ts;logIninpackages/cli/src/lib/cf/account.ts). Always pass--json.
The setup procedure
Step 1: ask which domain
Ask the person which domain (on Cloudflare) the Gateway should live under and propose apps.<domain>. The rules, from packages/cli/src/commands/setup/deployGateway.ts and runSetup.ts:
- Any hostname under one of the account's active zones works. Apps get one label below it: the Gateway at
apps.example.computs an app attodo.apps.example.com. - Under
--json, a new install needs--domain: without it setup fails withDOMAIN_REQUIRED("Choose the Gateway's address with --domain",data.zoneslists the account's active domains). Once the install row exists--domainis optional: the host is remembered, and an install that stopped before its first upload may still move to another name. - The Gateway can't move once it is live:
--domainnaming another host fails withDOMAIN_CHANGE_UNSUPPORTED, whosedata.rerunis the command without--domain. - A zone apex (
--domain example.com) is allowed with the warningGATEWAY_AT_ZONE_APEX: the sign-in cookie covers every subdomain and apps take names directly under the domain;data.suggestedDomainisapps.example.com. - A
--domainthat isn't a full hostname is refused before anything is asked of Cloudflare.
Step 2: run setup
pnpm everyapp setup --domain apps.example.com --jsonFlags (packages/cli/src/commands/setup/command.ts):
| Flag | Effect |
|---|---|
--domain <host> |
Where the Gateway lives (step 1). Remembered in the install row after the first write. |
--account-id <id> |
The account to find the Gateway in, or to install it in; default CLOUDFLARE_ACCOUNT_ID, else the only account the login sees. |
--force-dns |
Publish new hostnames even where a DNS record answers them (the record is replaced). A takeover flag: only on the person's word. |
--allow-downgrade |
Hidden. Deploy a Gateway older than the live one (a deliberate rollback). Only on the person's word. |
--redeploy |
Hidden. Upload the Gateway even when it is up to date (after a change in the dashboard, such as a key rotation). |
--yes, -y |
Skip everyapp's own confirmations (never cf's questions, or the one before replacing a Worker changed outside everyapp). Answers No to "Configure browser app installs?" (a CF_DEPLOY_TOKEN already set stays); Cloudflare Access then goes on without a question when the account can (as under --json). |
--no-access |
Leave Cloudflare Access off: by default setup (and a deploy that installs a Gateway) puts it in front once the owner exists. pnpm everyapp access off takes it off later, access on puts it on. update has no such flag: it never offers Access. |
--json |
NDJSON events, never a prompt. |
--verbose |
Every step and progress line; a checkout build's output streams to stderr (it otherwise goes to a log file). |
Setup asks for no go-ahead: its first lines say which account and where. Running it again any time picks up where it stopped and does nothing once everything is there. Step by step for an agent: packages/cli/skills/everyapp/setup/gateway.md (in an app .claude/skills/everyapp/setup/gateway.md).
Step 3: read the events
In order, from packages/cli/src/commands/setup/runSetup.ts, deployGateway.ts, packages/cli/src/lib/cf/account.ts, packages/cli/src/lib/gateway/discover.ts and packages/cli/src/lib/cf/deploy.ts. "Your move" is empty when the run goes on by itself.
| Step | Events | Your move |
|---|---|---|
setup.toolchain |
step ok "Node …, cf …" with data.node, data.cf. |
|
setup.auth |
Not logged in: action_required cloudflare_login, data.waiting: true, data.url, data.code, data.expiresAt (now + 5 minutes), data.waitingFor: "Cloudflare login" (cf runs auth login --no-browser). Then step ok "Logged in to Cloudflare as email" with data.email (null for an API token). Not approved in 5 minutes: error CLOUDFLARE_LOGIN_FAILED, retryable, rerun pnpm everyapp login. With CLOUDFLARE_API_TOKEN set but refused: CLOUDFLARE_TOKEN_REJECTED. |
Hand off the link and code; keep reading. Unattended runs (CI) set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID instead; the token needs Account › Workers Scripts › Edit, Account › D1 › Edit, Account › Workers KV Storage › Edit, Zone › Zone › Read (listing the domains) and Zone › DNS › Read, limited to the account and the domain's zone (CLOUDFLARE_TOKEN_REJECTED's list; https://dash.cloudflare.com/profile/api-tokens). The device login is broader: cf asks for its full default scope set across every account the person belongs to (DNS edit and registrar included); everyapp uses only Workers, D1, KV, the zone list and DNS. If the person asks what they are approving, say so. Without DNS read, the domain step stops at DNS_UNREADABLE (fix "give the API token Zone → DNS → Read"); its only rerun is the takeover --force-dns, which skips the record check and replaces any record in the way. To separate the login from setup, run pnpm everyapp login --json first (step cloudflare.login). |
| discovery | Several accounts and no Gateway anywhere, or a Gateway in several accounts that --domain doesn't single out: action_required choose_account (exit 2), data.choices[] with one rerun per account (--account-id <id>, or for several Gateways --domain <host>); among Gateways each choice also has gatewayUrl, and the headline is "Choose which Gateway to use" (packages/cli/src/lib/gateway/discover.ts). ACCOUNT_NOT_ACCESSIBLE when --account-id names one the login can't use; ACCOUNT_REQUIRED when the login lists none. |
Ask the person which; run that choice's rerun (not the original command). |
setup.account |
step ok "Setting up EveryApp in name" with data.id, data.name, data.email, data.gateway (true when a live Gateway was found). |
|
| release | The release this checkout builds is described, not built (checkoutSource in packages/cli/src/lib/gateway/checkoutRelease.ts: the Gateway package's version, the sources' tree hash, the commit when the version alone doesn't say what it is, the newest migration); nothing is emitted. It names the release in setup.gateway's start message and feeds the downgrade guard before the build. The description carries no minCli (a release says it once built), so the CLI_TOO_OLD check runSetup.ts makes at this point never fires for a checkout: it comes under setup.gateway, after the build. |
|
setup.plan |
New installs only. Paid: step ok "Account is on Workers Paid". Free: action_required upgrade_workers_paid (exit 2), data.url https://dash.cloudflare.com/<account>/workers/plans, data.plan: "free"; nothing was created. Unknown: warning WORKERS_PAID_UNVERIFIED and the run goes on. Unreadable: step skip "Couldn't read this account's plan; Cloudflare refuses the upload if it's on Free". |
Free: hand off, then the same command. Unverified: carry on, and tell the person to check the subscription. |
| domain | No active zone: action_required add_domain (exit 2): "Add a domain to this Cloudflare account" with the registrar URL, or "Finish activating zone on Cloudflare" with the zone's dashboard URL (data.zone, data.status). --domain under no domain of the account: error ZONE_NOT_FOUND with data.zones and data.url. Then a new host's DNS is checked (step gateway.domains): DNS_RECORD_CONFLICT ("… already points somewhere else", fix "delete the record in Cloudflare (zone → DNS), or use another name", data.rerun with --force-dns, data.authorize: true), DNS_UNREADABLE (same takeover), ZONE_CUSTOM_DOMAIN_LIMIT (more than 100 Custom Domains in the zone). The Gateway's own Custom Domain from a run that stopped after publishing it (Cloudflare's placeholder record AAAA 100:: at the name, the Worker every-app-gateway holding the hostname in cf workers list) is no conflict: progress "host is already this Gateway's Custom Domain (an earlier run published it, then stopped)" and the run goes on. Two warnings need nothing: CUSTOM_DOMAIN_OWNER_UNCHECKED ("If another Worker already serves host, it will be replaced by this deployment.", data.hosts) comes with every new hostname under --json that isn't the Gateway's own already, because which Worker holds a hostname with no record in the way isn't read and cf only asks before taking one when it runs at a person's verbose terminal (cfAsks is interactive and --verbose; the human sink hides this warning, the NDJSON sink writes it); ZONE_CUSTOM_DOMAIN_LIMIT_NEAR (data.total, data.limit) means the zone holds 95 or more of Cloudflare's 100 (packages/cli/src/lib/domains/preflight.ts). Warning GATEWAY_AT_ZONE_APEX at an apex. Then step setup.domain ok "Apps will live one level below appBaseDomain" with data.gatewayHost, data.appBaseDomain, data.zone. |
add_domain: hand off, then the same command. ZONE_NOT_FOUND: ask for a name under one of data.zones. DNS_RECORD_CONFLICT: ask the person to delete the record or pick another name; never add --force-dns yourself. |
setup.gateway |
step start "Installing the Gateway at host (release)" or "Upgrading the Gateway to …", data.change one of install, resume (an install that stopped before its first upload, or after it before the row recorded its hostname), upgrade, repair, redeploy; data.release, data.releaseName, data.from, data.gatewayUrl, data.account; for install, data.expectedMinutes: { min: 5, max: 10 }, how long until the owner link (a person's line ends "(usually 5–10 minutes, then the owner link)"). Before it, a Gateway older than the live one stops with DOWNGRADE_REFUSED (below). After it the release is opened (gateway.release: the build, or the cache hit) and its manifest's minCli is checked before anything else (refuseNewerThanCli in packages/cli/src/lib/gateway/release.ts): CLI_TOO_OLD ("Gateway … needs everyapp … or newer (this is …)", fix "update everyapp (in a checkout: git pull), then run this again", data.minCli, data.cliVersion) stops before gateway.resources; nothing is created or changed. minCli is MIN_CLI_VERSION in apps/every-app-gateway/scripts/build-release.ts, compared with packages/cli/package.json's version; both come from this checkout, so it fires only when the Gateway's sources ask for a newer everyapp than the checkout's CLI. |
A checkout build takes a minute or two. CLI_TOO_OLD: git pull && pnpm install, same command. |
gateway.release |
step start "Building the Gateway from this checkout (usually 1–2m)" (data.dir), then ok "Built the Gateway version from this checkout." (data.cached: false); or, when the build cache has it for the sources' tree hash, ok "Using the Gateway version built from this checkout before (…)" (data.cached: true); both with data.treeSha256 and data.release. A failed build is GATEWAY_BUILD_FAILED (fix "fix the build error above, then run this again"). |
|
gateway.resources |
step ok "Created …" or "The Gateway's database and KV namespace are there". |
|
gateway.lease |
progress "Waiting for … to finish (…)" while another run holds the Gateway; step ok "The other … finished." After the wait runs out: error LEASE_HELD, retryable (below). |
|
gateway.migrations |
step ok with data.applied, data.fresh, and when a restore point was saved first data.restoreCommand (pnpm everyapp db restore --gateway) and data.rollbackCommand. |
|
gateway.deploy |
The upload. Failures: action_required register_workers_subdomain (exit 2, data.url, the account's first Worker; cf stops after the version and its Custom Domain are live, so the next run finds the Worker holding the hostname), action_required upgrade_workers_paid ("Cloudflare refused the upload on the Free plan"), action_required rerun_interactive (the Worker was changed outside everyapp: cf won't overwrite it unattended). Before it, step gateway.resume ok "Replacing the Gateway an earlier run uploaded at host" (data.gatewayHost) when the install row names no version yet and the Worker holds the row's hostname as a Custom Domain: that upload is this install's own, replaced without a terminal, and this upload sets the Gateway's key pair and records it (a Worker by that name holding some other hostname still needs rerun_interactive). |
Hand off, then the same command. rerun_interactive: the person runs data.rerun at their own terminal. |
gateway.tls |
progress while the new hostname's certificate is issued, up to 10 minutes (DEFAULT_MAX_WAIT_MS in packages/cli/src/lib/domains/certWait.ts). |
|
setup.gateway |
step ok "The Gateway (release) is live at url" with data.versionId, data.release. Already on this release: step skip "The Gateway is up to date with …" (data.upToDate: true, data.apps), or a repair ok with data.repaired: true, data.added, data.apps, data.was. |
|
setup.owner |
Owner exists: step skip "Owner account is email" (data.email, data.organizationName). Otherwise one plain request first: warning GATEWAY_UNREACHABLE (data.kind tls, dns or other: the link works as soon as the host answers) or ACCESS_IN_FRONT (whoever opens the link signs in to Access first). Then action_required claim_owner, data.waiting: true, data.url (https://<gateway>/setup#token=est_…), data.expiresAt (24 hours), data.replaced (an earlier unused link stopped working), data.waitingFor: "the owner account (up to 15 minutes)". The run polls the link's row every 5 seconds for 15 minutes. Claimed: step ok "Owner account created for email". |
Hand off the link; keep reading. Only the person opens it. The link is shown to you because --json relays it; it is never written in CI (CI set): there setup ends done (exit 0) with warning OWNER_NOT_CLAIMED, data.rerun pnpm everyapp setup "at a terminal" and data.needsTerminal: true: hand that command to the person as it is, never with --json. |
setup.owner (stop) |
Nobody opened it in 15 minutes: action_required claim_owner (exit 2) "Open the setup link above to create the owner account … It still works for …; running setup before it's opened replaces it", data.setupUrl, data.expiresAt, data.rerun pnpm everyapp setup --json "to finish" (--json because this run had it; run it as it is). The link was replaced or expired meanwhile: claim_owner "Use the newer setup link, or get a new one", the same rerun "for a new link". |
Ask whether the person opened it. Yes: run setup again; it finds the owner. No, or lost: run setup again for a new link (the old one stops working). |
| browser installs | Interactive only, not with --yes (it answers No), self-hosted only, and only when the Gateway Worker has no CF_DEPLOY_TOKEN yet (a token already set stays): "Configure browser app installs?" (default No), then a hidden Cloudflare API token prompt; the token is stored as the Gateway secret CF_DEPLOY_TOKEN. Never under --json. Then, on setup (and a deploy that installs a Gateway; never update), once the owner exists, on a self-hosted Gateway whose Worker lacks any of the three CLOUDFLARE_ACCESS_* settings, and without --no-access: Cloudflare Access. At a terminal: "Put Cloudflare Access in front of your Gateway?" (default Yes; context: one command, people sign in with their Cloudflare account or a code by email, the free Zero Trust plan covers 50 people, access off undoes it; when the Cloudflare login's email differs from the owner's it names both). Unattended (--json, --yes, CI): no question; it goes on when the account can, read before anything is written (turnAccessOnUnattended in packages/cli/src/commands/setup/runSetup.ts): a Zero Trust team exists (a new team's name is permanent and never picked for someone: without one the warning says so, data.rerun pnpm everyapp access on --team <zone's first label>), and the Cloudflare login's address is an owner or admin of the Gateway (Sign in with Cloudflare asserts that address, so the person running setup can get in; an API token has none; otherwise the warning names the address, data.rerun pnpm everyapp access on); a login that can't read Access gets the warning with cf's reason. Either way runs access on inside the run (the team name asked for at the terminal when the account has none): its events, then its done as the step setup.access ok with access on's message and data (Access). A failure there, and each skip above, is the warning ACCESS_SETUP_SKIPPED (data.rerun, data.rerunNote) and setup goes on to done. |
Skip. If wanted later: the person runs setup at a terminal, or pnpm everyapp secret set CF_DEPLOY_TOKEN --from-env CF_DEPLOY_TOKEN --json with the token already in the environment (runbook). |
done |
data.gatewayUrl, data.adminUrl, data.organizationName, data.owner (email, null in CI), data.release, data.mode (self_hosted, or hosted when the install row says so), data.gateway (installed, upgraded, repaired or up-to-date), data.accessOn (true when this run turned Cloudflare Access on, false when the question was declined, it was skipped unattended or access on failed, null when it wasn't offered: on already, --no-access, no owner yet, hosted, or update), data.facts, data.next, data.durationMs. A new Gateway or a new owner: facts Gateway and Admin ("invite people, choose who opens which app"), plus Access on ("everyone signs in through Cloudflare Access first") when accessOn is true, and next from nextAfterInstall in packages/cli/src/commands/setup/index.ts: self-hosted, pnpm everyapp create my-app "start your first app", pnpm everyapp access on "put Cloudflare Access in front of your Gateway (recommended)" (left out when accessOn is true), pnpm everyapp secret set OPENAI_API_KEY "AI for apps (or OPENROUTER_API_KEY, WORKERS_AI_API_TOKEN)", pnpm everyapp secret set EMAIL_FROM EMAIL_FROM_NAME "email invites and password resets; needs Cloudflare Email Sending"; a hosted Gateway (which ignores Access) gets three: pnpm everyapp create my-app, pnpm everyapp secret set OPENAI_API_KEY "turn on AI for apps", pnpm everyapp secret set EMAIL_FROM EMAIL_FROM_NAME "email invites; needs Cloudflare Email Sending". An upgrade: message "Upgraded to releaseName". |
Report (step 5). |
Step 4: resume
Exit 2 means a person has to do something; once they have, run data.rerun as it is: it is this same command, --json included when this run had it, and so is each choices[].rerun (choose_account names one per choice). Only a rerun that names another command (install_gateway names setup) is written without --json: add it to that one. Every step looks at what is there first and acts only on what is missing, so a rerun never repeats work:
- A login already approved, a plan already paid, a zone now active: the run passes those steps.
- An install that stopped before its first upload resumes from its install row (
data.change: "resume");--domainis optional and may still be another name. One that stopped after it (register_workers_subdomain: the Worker and its Custom Domain are live, the row names no version) resumes the same way: the hostname's placeholder record and the Worker count as this install's own (gateway.resume), with noDNS_RECORD_CONFLICTand no terminal. When the row did record the version but not the hostname, the rerun ischange: "resume"too, start "Finishing the Gateway's first publication athost(release)" and ok "The Gateway (release) is live aturl",data.gateway: "installed"indone;repairis for app drift on a Gateway that is live. - The owner link: each run that reaches the owner step with no owner mints a new link, and any earlier unused one stops working (
data.replaced: true, message "Open this new link…"). Don't rerun setup while the person is still about to open a link they have; rerun it when they report they opened it (the run finds the owner) or lost it (a new link). - A live Gateway on the same release:
setup.gatewayskip anddata.gateway: "up-to-date"; nothing is uploaded.
Step 5: show the result
From done.data: the Gateway URL (data.gatewayUrl), the admin URL (data.adminUrl, invite people and choose who opens which app), and the data.next commands. The person who claimed the link is signed in on that browser. On a phone: https://<gateway>/pwa. Then offer the next steps: an app (Create), AI keys (AI keys and app secrets), Access (next section), people (Invite people) and an AI client (Connect an AI client).
Recommended: put Cloudflare Access in front
everyapp access on|off|show (packages/cli/src/commands/access/command.ts and index.ts, with packages/cli/src/lib/access/plan.ts, record.ts and sync.ts; the person's page is /docs/access, the reference /docs/agent/access). It is the default: setup puts it in front once the owner exists, offered at a terminal ("Put Cloudflare Access in front of your Gateway?", Yes by default) and done without a question under --json or --yes when the account can (a Zero Trust team exists, the Cloudflare login is an owner or admin of the Gateway); when it can't, the warning ACCESS_SETUP_SKIPPED says why and the offer is yours: run pnpm everyapp access on --json (with --team <name> when the account has no team, the name picked by the person), or at a terminal. --no-access leaves it off (access off takes it off later). The Gateway signs people in by itself without it. Step by step for an agent: packages/cli/skills/everyapp/setup/access.md (in an app .claude/skills/everyapp/setup/access.md). What Access adds to the security boundary: /docs/agent/repo/threat-model (assets, actors, trust boundaries, one testable invariant per boundary).
pnpm everyapp access on --jsonWhat it does, from the Cloudflare login alone: finds or creates the Zero Trust team, keeps both ways to sign in (Sign in with Cloudflare for the members of the operator's Cloudflare account, and Access's one-time PIN, a code emailed to everyone else; each created when the team lacks it), keeps the allow policy "Every App gateway members" with every member's and invitee's email, puts one Access application in front of the Gateway's host and one in front of the apps' hosts (the Gateway's /mcp, the OAuth endpoints and /health, and each app's public routes and, where its manifest sets mcp, its /mcp, stay reachable without a login), and sets the three Gateway settings CLOUDFLARE_ACCESS_TEAM_DOMAIN, CLOUDFLARE_ACCESS_APPS_AUD and CLOUDFLARE_ACCESS_AUD in one new version. It is rerunnable and changes only what differs; it never removes anyone from the policy. The settings are written when one is missing on the Worker, an application was just created, nothing recorded what they were set to, or the record access on keeps in the Gateway's D1 (table _everyapp_access, packages/cli/src/lib/access/record.ts; values can't be read back from a Worker) names another team or other AUD tags (an application recreated by hand); done.data.settings says outcome set with that reason (missing, created, unrecorded, changed, or asked for --write-settings), else kept.
Handoffs and stops:
- No members or invitations yet, no
--email, and no policy from an earlier run:errorACCESS_NOBODY_ALLOWED("Nobody to let in yet … Claim the Gateway first, or name who will (the setup link is then behind Access too)."),data.rerunpnpm everyapp access on --email <your address>. Two ways on: finish setup first, or, when the person wants Access up before the claim, run that rerun with their address (the owner link is then behind Access). An--emailwith no account on the Gateway yet: warningACCESS_EMAIL_NOT_INVITED("Access will let them through, but the Gateway refuses anyone it doesn't know. Invite them in Manage workspace → Users.",data.emails). - The account has no Zero Trust team:
errorACCESS_TEAM_REQUIREDwithdata.suggested(the zone's first label). The team name is permanent: its sign-in page is<name>.cloudflareaccess.comfor good. Ask the person (table above), then rerun with--team <name>. Lowercase letters, digits and dashes (ACCESS_TEAM_INVALID); a name taken elsewhere on Cloudflare fails withACCESS_TEAM_CREATE_FAILED: ask for another. - A hosted Gateway:
errorACCESS_HOSTED; hosted Gateways ignore the Access settings. done.data.signIn:url,pending(the emails of members who have a password and no Access identity yet) andfirst(both: Access's chooser between Sign in with Cloudflare and the emailed code, the usual case;pin;idp). Tell the person: each of those people opens the Gateway once, picks Sign in with Cloudflare if they are a member of the Cloudflare account (it asserts that account's email, which must be their Gateway address:ACCESS_LOGIN_NOT_MEMBERwarns when the login's isn't) or gets a code from Access by email, then signs in with their Gateway password as usual; after that Access alone signs them in.done.data.signInMethodslists the providers;done.data.identityProviderssays which of the two this run created.done.data.nextlistspnpm everyapp secret set EMAIL_PASSWORD_SIGN_IN(false, once everyone has linked Access) and, with--email-domain,CLOUDFLARE_ACCESS_AUTO_JOIN_DOMAINS; run either only when the person asks.- Run
access onagain after the person invites someone, so Access lets them in.access show --jsonreports without changing anything:data.on, anddata.fixeslistspnpm everyapp access onwhen something differs from whatonwould make.access offasks first ("Turn Cloudflare Access off forhost?"); under--jsonwithout--yesit iserrorCONFIRMATION_REQUIRED("Turning Access off needs confirmation at a terminal, or --yes") with--yesindata.rerunanddata.authorize: true: add--yesonly on the person's word. - Behind Access,
setupwarnsACCESS_IN_FRONTbefore the owner link. A deploy's live wait can't see past Access either, so once Access answers its request it only reads the app's row while the Gateway's per-minute cron makes the app live (progress"Cloudflare Access answers before the Gateway; waiting for the Gateway to makelive (it looks every minute)"), and ends live like any other deploy; only after 10 minutes still deploying does it warn ACCESS_IN_FRONT("The deploy stalled",data.rerunstatus) and skipapp.live. - After
app.live, every deploy (anddelete) keeps the apps application in step when one exists (syncAppsAccessAfterinpackages/cli/src/lib/access/sync.ts):stepapp.accessok "Cloudflare Access letsappId's<public routes | /mcp | public routes and /mcp>through." or skip "Cloudflare Access already matchedappId's<…>." (data.changed,data.removed; the words follow what the manifest opens: itspublicroutes, its/mcpwhere it setsmcp, or both). A login that can't read Access warnsACCESS_NOT_CHECKED(only when the app has public routes or setsmcp), a failed updateACCESS_SYNC_FAILED, both withdata.rerunpnpm everyapp access on; neither fails the deploy, and nothing is emitted when Access isn't in front.
Create, run, deploy, share
Create
pnpm everyapp create ../<name> --jsonFrom packages/cli/src/commands/create/index.ts, command.ts and steps/appId.ts:
- The default: run it from the every-app folder with a path beside it,
../<name>. The app is made there as its own git repository (one repository per app), linked to the checkout: itspackage.jsontakes the SDK and the CLI bylink:path, so neither folder moves. For someone with many apps, a folder of apps inside the checkout also works: a barepnpm everyapp create <name>from anywhere in the checkout makesapps/<name>, untracked by git, no repository of its own. - The argument is a path when it has a slash or starts with
.,~or/(isPathArgumentinsteps/appId.ts): resolved from the current directory,~being the home directory, and its last part is the name. A path outside the checkout is the "anywhere else" rule above;apps/<name>inside it (or../../apps/<name>frompackages/cli) is the inside rule; any other place inside the checkout is refused,APP_DIR_IN_CHECKOUT, with adata.fixnaming the two places it can go. A bare name goes inapps/<name>inside the checkout and in./<name>anywhere else. - The name becomes the app id and part of its address,
<name>.<app base domain>. Rules:^[a-z]([a-z0-9-]*[a-z0-9])?$, 64 characters or fewer, no--, not reserved for the platform. A bad name iserrorINVALID_APP_IDwith adata.fixthat suggests a slug ("Analytics_App" becomesanalytics-app; for a path, its last part). Under--jsonthe name is required:APP_ID_REQUIRED. - Run from inside an app:
NESTED_APP. The directory exists:APP_DIR_EXISTS. - Steps:
create.copy(the starter,starter),create.skill(the everyapp and build-every-app skills copied to.claude/skills/<name>and.agents/skills/<name>, andAGENTS.mdpointing at both: "Before running everyapp, read the everyapp skill:.agents/skills/everyapp/SKILL.md" and "To build or change this app, read the build-every-app skill:.agents/skills/build-every-app/SKILL.md"),create.install(a failure isINSTALL_FAILEDwith reruncd <dir> && pnpm install: the app exists, only the install is left),create.types(warningWORKER_TYPES_FAILED;pnpm devtries again),migrations.local(warningMIGRATIONS_FAILED), and outside the checkoutcreate.git(warningGIT_FAILED). done.data:appId,dir(absolute),next(cd <dir>with the path to type from where create ran,cd ../<name>for the default and the whole path when it was given absolute or from~;pnpm dev"run it locally";pnpm run deploy"put it live"; the app'spackage.jsonscriptsdevanddeployruneveryapp devandeveryapp deploy,starter/package.json). Nothing is deployed: the deploy is the next command, from the app's directory (Deploy).--yesasks nothing (a name is then required).--verbosestreams the install and git output to stderr.
Then cd into it (the next line) and work in its src/; the manifest is everyapp.config.ts; pnpm dev runs it (Run locally). The Gateway handles sign-in: never add a login page.
Run locally
pnpm everyapp devFrom the app's directory (pnpm dev runs the same thing). It is long-running and human-facing, and takes no --json: start it in the background and read the printed URL, http://<app-id>.localhost:8787 (--port, or $PORT, default 8787). A busy port is error DEV_PORT_IN_USE ("Port 8787 is already in use") with data.rerun pnpm everyapp dev --port <free> (data.free; packages/cli/src/commands/dev/gatewayServer.ts). The person is signed in as a seeded dev user. Stop it with Ctrl+C (SIGINT) to the everyapp process, which signals cf dev's own process group; never a pattern kill of vite or workerd, which hits other projects too (pnpm prints ELIFECYCLE after a clean Ctrl+C). A pnpm install or a deploy while dev runs rebuilds packages/sdk/dist (the root postinstall) and breaks dev for a few seconds. Reference: /docs/agent/dev.
Deploy
pnpm everyapp deploy --jsonFrom the app's directory; an agent runs pnpm everyapp deploy --json there. pnpm run deploy is the same command as a person runs it, the user docs' spelling (the template's package.json script deploy is everyapp deploy). Nothing is asked first: the first line, step app.deploy, says "Deploying appId to gatewayHost (organizationName)". From packages/cli/src/commands/deploy/command.ts, index.ts, deployApp.ts and steps/offerGateway.ts:
- No Gateway in any account the login sees:
action_requiredinstall_gateway(exit 2): "Install a Gateway first:accounthas none yet. Apps deploy behind one. Setup needs a person once, to create the owner account."data.rerunispnpm everyapp setup --domain apps.<zone>(the account's first active zone, or the placeholder<hostname>when it can't tell), withdata.accountsanddata.domain. Ask the person, run the setup procedure, then deploy again. At a terminal the same deploy asks "Install a Gateway at apps.zone?" and runs setup inline, owner link included;--yesinstalls there without asking; declining isCANCELLED(exit 1, nothing deployed). - Login under
--json:cloudflare_loginon stepapp.login, as in setup. - Which Gateway: the one the login finds in its accounts; its URL always comes from the install row.
--gateway <url>(orEVERYAPP_GATEWAY_URL) and--account-id <id>only pick among several (requestedGatewayUrlandconnectToGatewayinpackages/cli/src/lib/gateway/connect.ts). A URL whose host isn't the Gateway found:errorGATEWAY_URL_MISMATCH("The Gateway here isfound, nothost", "Forhost, pass its account with --account-id"), withdata.rerunthe command without--gateway, or when the URL came from the environment the fix "unset EVERYAPP_GATEWAY_URL to usefound" (checkGatewayUrlinpackages/cli/src/lib/gateway/discover.ts). Not a full URL:INVALID_GATEWAY_URL. - Steps:
app.connect,app.register(a first deploy carries the access facts below),app.domain(a new hostname's DNS check, before anything is created: the sameDNS_RECORD_CONFLICT,DNS_UNREADABLEandZONE_CUSTOM_DOMAIN_LIMITas setup,--force-dnsthe takeover, and the warningsCUSTOM_DOMAIN_OWNER_UNCHECKEDandZONE_CUSTOM_DOMAIN_LIMIT_NEARas in setup's domain row;preflightAppHostinpackages/cli/src/lib/domains/preflight.ts),app.resources(D1, KV and Hyperdrive created or found,progress"Created …"; warningRESOURCE_RECREATEDmeans a database or namespace the app used before is gone from the Cloudflare account and a new, empty one was made: tell the person),app.install,app.build,app.schema(drizzle migrations that don't describe the schema stop the run before anything changes:errorSCHEMA_DRIFT, headline "The schema has changes no migration covers",data.rerunpnpm db:generatewithdata.rerunNote"then run this again", in the app's directory;packages/cli/src/commands/deploy/steps/checkSchemaDrift.ts),app.migrations(a restore point first;pnpm everyapp db restore --jsonundoes it, only when the person asks),app.upload, then for a new app or a new hostnameapp.bind, withgateway.upgrade("Upgrading the Gatewayfrom→to") nested inside it, betweenapp.bindstart and ok, when the checkout's Gateway version differs from the live one (an older one is refused:DOWNGRADE_REFUSED), thenapp.live, thenapp.accesswhen Cloudflare Access fronts the apps (above). A redeploy of an app the Gateway already binds has noapp.bind: it goes fromapp.uploadtoapp.live(bindIfNeededinpackages/cli/src/commands/appGateway.ts). The other steps (app.email,app.hyperdrive,app.helper,app.workflows): /docs/agent/deploy. - Takeover errors, each with
data.authorize: trueand the flag indata.rerun:DNS_RECORD_CONFLICT(--force-dns),DURABLE_OBJECT_REMOVED(--allow-delete-durable-object-data),WORKFLOW_REMOVED(--allow-delete-workflows),EMAIL_ROUTING_DISABLED(--enable-email-routing),CONFIRMATION_REQUIRED(--yes). Show the error, ask, and rerun with the flag only on yes.DOWNGRADE_REFUSEDwhen the checkout is behind the live Gateway:git pull, same command.rerun_interactivewhen the app's Worker was changed outside everyapp. app.live(packages/cli/src/commands/deploy/steps/waitForLive.ts,awaitLiveinappGateway.ts): the deploy waits up to 10 minutes for the registry to sayactive, requestinghttps://<hostname>/every 3 s, with aprogressline every 15 s (data.waitedMs,data.answered). Live:stepok withdata.urland, when the wait showed,data.waited(certificateorlive). Not live: awarningsays why, thenstepskip with nodata.reason:CERT_PENDING(no answer in 10 minutes; "goes live on its first visit once it has one",data.hostname,data.fixabout the zone's CAA records),ACCESS_IN_FRONT(10 minutes behind Cloudflare Access with the Gateway's cron never making it live: "The deploy stalled",data.rerunpnpm everyapp status), orAPP_DISABLED(switched off in Admin → Apps). The Gateway answered but the app never wentactivein 10 minutes:errorAPP_NOT_LIVE, retryable,data.rerunpnpm everyapp status"then deploy again". With--no-waitthere is no wait: ok at once for an app that was live before this redeploy, otherwise skip withdata.reasoncertificate(a new hostname) orfirst-visit(a hostname already published).data.reasonoutside-base(the hostname isn't one label below the app base domain; warningAPP_HOST_OUTSIDE_BASE, never routed, never waited for). WarningLOCAL_DNS_STALE: live, but this computer's resolver has no address for it yet.- After the upload, step
app.secretswarnsAPP_NEEDS_SETUP("<app>won't open until<names>is set", the first unmet setting'swhyunder it,data.setup) when a required setting is unset, andnextgets the one command that opens it,pnpm everyapp configure <app>(note "asks for each;<app>opens once they are set"). The run goes on; the app is live but answers its setup gate until then (/docs/agent/manifest). done.data:url,workerName,versionId,live(boolean),organization,publicRoutes([{ path, methods }], what the manifest opens to the internet),next.- Safe to repeat; a redeploy of an app the Gateway already serves never touches the Gateway.
- Someone else's app from GitHub, no clone:
pnpm everyapp install https://github.com/<owner>/<repo> --jsondeploys its latest release'severyapp-release.tar.gz.NO_RELEASE: clone it,pnpm installin it, then this checkout'spnpm everyapp deployfrom its directory (a clone's own CLI copy can't deploy: a deploy updates the Gateway, which only a checkout builds). Never build a repository from its URL yourself.
What to tell the person after a deploy
Three facts the human sink prints, which you reconstruct from the events (packages/cli/src/lib/output/events.ts, milestones app.live and app.register); formatFacts (packages/cli/src/lib/output/table.ts) pads every label in the block to the longest one, so under Who can open the Live and Public lines print wider than these samples:
Live https://<app>.<base>fromapp.liveok (data.waited: "certificate"addsCertificate issuedabove it). Not live yet:URL …, plus aStatusline only when the skip carriesdata.reason(a--no-waitrun):Goes live on its first visit, once its certificate comes (a few minutes)forcertificate,Goes live on its first visitforfirst-visit;outside-baseprintsLive Not routed. After a waited run that stopped onCERT_PENDING,ACCESS_IN_FRONTorAPP_DISABLED, the skip has noreason: tell the person what that warning said.Who can open … · change it at https://<gateway>/admin/appsfromapp.registerok on a first deploy (data.firstDeploy: true):data.scope: "shared"→ "Members of every organization (shared app)";data.defaultAccess: true→ "Everyone inorganizationName";data.deployerAccess: false→ "Private to owners and admins"; otherwise "Private to you, owners and admins".data.hasOwner: falseadds "The Gateway has no owner yet; its first owner gets the app (pnpm everyapp setup)".Public Open to the internet without sign-in: GET /hooks/*, POST /collectfromapp.registerokdata.publicRoutes(also ondone.data): the manifest'spublicroutes, the first six, then… and N more (everyapp.config.ts public); nothing when the manifest has nopublic. Say it: those routes answer without sign-in.- The admin URL
https://<gateway>/admin; the app shows under All apps on the Gateway. A redeploy keeps the app's data. - Warning
APP_NEEDS_SETUPmeans a required setting is unset: the app shows Needs setup on the Gateway and opens its setup dialog instead, and its tools stay out of/mcpuntil it is.done.data.nextcarries the one command,pnpm everyapp configure <app>(note "asks for each;<app>opens once they are set"): the person runs it at a terminal, or an agent pipesNAME=valuelines into it or adds--from-env(AI keys and app secrets); it ends with<app> is ready, andpnpm everyapp secret list <app> --jsonshowsdata.app.setup.ready: true. - Then
pnpm everyapp status --json: itsdata.nextsays what to do with the app now that it is live (invite people, connect an AI client, an AI key the app runs without), each with awhyto pass on (Verification checklist).
Invite people
Invitations are in the Gateway UI, not the CLI (/docs/people): Manage workspace at the bottom of the sidebar → Users → Invite user, email and role (Member opens the apps they're given; Admin also invites and manages apps). Without email set up the UI offers Copy invite link, which expires after 72 hours. Who opens an app: Apps → ⋯ → Manage access, or Edit app → Available to everyone. Deactivate removes someone everywhere at once.
Email, so invites and resets go out by mail: the person onboards the domain in the Cloudflare dashboard (Compute & AI → Email Service → Email Sending → Onboard Domain), then the Gateway settings are set with secret set: EMAIL_FROM (an address at that domain) and a display name, from EMAIL_FROM_NAME or embedded as EMAIL_FROM="Name <address>"; without both the Gateway has no sender (parseEmailSender in packages/sdk/src/internal/email.ts). Under --from-env one name per command (SECRET_SEVERAL_NAMES otherwise). Step by step: packages/cli/skills/everyapp/setup/email.md (in an app .claude/skills/everyapp/setup/email.md); the person's steps are in /docs/people.
Connect an AI client
The person connects Claude, ChatGPT, Claude Code or Cursor to the Gateway's one MCP address, https://<gateway>/mcp, once: the assistant signs in to the Gateway as them and asks which apps it may use. The steps per client are in /docs/connect-ai (Claude Code: claude mcp add --transport http everyapp https://<gateway>/mcp, then /mcp → everyapp → Authenticate); a tool that can't sign in takes a personal access token from Admin → Tokens → New token as Authorization: Bearer. Connections are listed and revoked under AI connections (https://<gateway>/settings/connections). It works behind Access. Reference: /docs/agent/mcp.
AI keys and app secrets
pnpm everyapp configure <app> --from-env --jsonFrom packages/cli/src/commands/configure/index.ts, packages/cli/src/commands/secret/index.ts and packages/cli/src/lib/secretValue.ts:
pnpm everyapp configure <app>sets up everything the app's manifest declares (its AI providers, capabilities and own secrets): from anywhere once the app is deployed (its deployed manifest), or in its directory before that, for the Gateway's settings. At a terminal it shows what is set (names only, never values), asks for each required setting that isn't with why and where to get it, asks which member of an any-of group, offers the optional ones, and sets each where it is held in one go. One name by itself ispnpm everyapp secret set NAME [--app <id>](pnpm everyapp secret set OPENAI_API_KEY --from-env OPENAI_API_KEY --jsonfor an agent). Reference: /docs/agent/secrets.OPENAI_API_KEY(andOPENROUTER_API_KEY,EMAIL_FROM, …) are Gateway settings: set from any directory, live at once, no deploy. Every app that uses AI goes through the Gateway's key. Step by step per provider:packages/cli/skills/everyapp/setup/openai.md,setup/openrouter.md,setup/workers-ai.md,setup/dataforseo.md,setup/context.md(the indexsetup/README.mdlists them all; in an app under.claude/skills/everyapp/setup/).DATAFORSEO_API_KEYcan also back an MCP app:pnpm everyapp mcp on dataforseo --jsonoffers DataForSEO's tools to members' AI clients at/mcpwith the Gateway's key, nothing to deploy; it shows Needs setup until the key is set (PROVIDER_KEY_UNSETnames it).mcp list --jsonsays which are on and whether each key is set. Reference: /docs/agent/mcp.- A value never comes from the command line (
configure NAME=valueis refused,VALUE_ON_COMMAND_LINE; a second argument after the app id, or--from-env=<value>, is refused by the argument parser before that,TOO_MANY_ARGUMENTSorINVALID_VALUE, with the value cut out or never shown and the lineA value never goes on the command line: it lands in the shell's history and the process list. Rotate it if it was real.; nothing is set). Under--jsonthere is no prompt:configure <app>takesNAME=valuelines on stdin (one per line; blank lines and#lines skipped, anexportprefix allowed, one pair of quotes around a value dropped, the value otherwise as it is; a line that isn'tNAME=valueisPIPED_LINE_INVALIDand never echoed, since a bare value piped in may be a secret; a name the manifest doesn't declare isSECRET_NOT_DECLARED, echoed only when it looks like a setting name; a blank valueSECRET_EMPTY; each refused before anything is written) or, with--from-env, each unset setting from the environment variable of its own name (DATAFORSEO_API_KEYfrom$DATAFORSEO_API_KEY; a set name is never replaced, so a pair with one name set gets only the other; stdin isn't read); the two never combine.secret set NAMEtakes--from-env VARor one piped value. With nothing to read and a required setting unset:errorSECRET_VALUE_REQUIRED("Can't ask for's settings without a terminal", then " needs . Run this at a terminal, pipe NAME=value lines in (one per line), or export them under those names and add --from-env (which also takes the variable of any unset optional setting).", data.namesthe names still missing,data.rerunpnpm everyapp configure <app> --from-envonce deployed,pnpm everyapp configure --from-envin the app's directory before its first deploy; fromsecret set: "Can't ask for …'s value without a terminal",data.rerunpnpm everyapp secret set NAME --from-env NAME; its variable unset:SECRET_EMPTY). The value is the person's: if it isn't already in your environment, hand them the interactive command to run themselves (table above). Never ask for it in chat, never print it. - An app's own secret (
GITHUB_TOKEN, …) must be listed undersecretsin itseveryapp.config.tsand is set after its first deploy:configure <app>from anywhere, orsecret setfrom the app's directory or from anywhere with--app <id>(against the manifest its last deploy stored). Before that deploy,configurein the app's directory sets the Gateway's settings and leaves the app's own for after it (nextpnpm everyapp deploy, thenpnpm everyapp configure <id>for them; while a Gateway setting is still missing,nextispnpm everyapp configurewith notein the app's directory: …; a piped own secret isAPP_NOT_FOUND, "idisn't deployed yet. Its own NAME can be set after its first deploy. Nothing was set."), andconfigure <id>by name isAPP_NOT_FOUND("idisn't deployed to this Gateway",data.rerunpnpm everyapp status; a row whose first deploy never finished,status: "pending", counts as undeployed). Fromsecret setbefore that deploy:errorAPP_NOT_FOUND("idisn't deployed yet. Its secrets can be set after its first deploy. Nothing was changed.",data.rerunpnpm everyapp deploy). A name the manifest doesn't list:SECRET_NOT_DECLARED, whosedata.fixquotes thesecrets: […]line to add and whosedata.rerunispnpm everyapp secret set <name>; noeveryapp.config.tsin the directory and no--app:SECRET_NOT_DECLAREDwith fix "cd into the app's directory and run this again, or add --app"; --appnaming an app that isn't deployed:APP_NOT_FOUND("idisn't deployed to this Gateway") withdata.rerunpnpm everyapp status; a Gateway setting or declared name typed in another case or without its underscores:SECRET_NOT_DECLAREDwithdata.didYouMeanand a correcteddata.rerun. Later deploys keep it. Fordev, the person puts it in the app's.dev.vars. pnpm everyapp secret list <app> --json(from anywhere; in the app's directory the id can be left out) names what is set and what the app still needs, never values:data.app.setuphasready,unmet(each withnames,holder,need.kind,why,docs,setup, the guide file) andcommand,{ command: "pnpm everyapp configure <app>" }, the one command that opens the app (nullonce it is ready).pnpm everyapp status --jsoncarries the same per app asdata.apps[].setup, and listsAPP_NEEDS_SETUPinfixes. Reference: /docs/agent/secrets, the shape on /docs/agent/json.
Keeping things up to date
- The Gateway, in the checkout:
git pull && pnpm install, thenpnpm everyapp update --json(packages/cli/src/commands/update/command.ts;setup --jsondoes the same). It takes setup's flags without--domain, upgrades the Gateway where it is (migrations first, with a restore point) and keeps its data, users and apps. It never installs one: with none,GATEWAY_NOT_FOUNDwith rerunpnpm everyapp setup --domain <hostname>; an install that never uploaded isGATEWAY_NOT_INSTALLED.done.message"Upgraded toreleaseName" anddata.gateway: "upgraded"; already current:setup.gatewayskip anddata.gateway: "up-to-date". On a live self-hosted Gateway behind Cloudflare Access,update(andsetup) then brings both Access applications in line with what this release lets through (its bypass paths, the sign-in methodsaccess onvouches for, the policy), under theaccesslease, as stepgateway.access(okwhen one changed,skipwhen both matched; Cloudflare Access): a rollout that moves a Gateway path never leaves the live bypass list behind. Whether Access is on is read from the live applications, soaccess offstays off; a failure there is the warningACCESS_RECONCILE_FAILED(orACCESS_NOT_CHECKEDwhen the applications couldn't be listed) withdata.rerunpnpm everyapp access on, never a failed update; an application deleted by hand isACCESS_APPLICATION_MISSING(nothing is created: that isaccess on's job). The Gateway's three settings aren't rewritten byupdate: an AUD tag that changed isaccess on's to fix. - A deploy that rebinds the Gateway (a new app or a new hostname) or a delete also upgrades it when the checkout is newer, because the rebind deploys the checkout's Gateway (
gateway.upgradeinsideapp.bind). A redeploy of an app the Gateway already binds never touches it, however new the checkout is;statusthen listsGATEWAY_OUTDATED("Your every-app folder has a newer Gateway than the one online", detail "Run update now, or leave it: the next new app's deploy updates it too.") asseverity: "info", withrunpnpm everyapp updateandnote"updates it now"; it comes last infixesand doesn't count inmessage(packages/cli/src/commands/status/problems.ts). DOWNGRADE_REFUSED(packages/cli/src/lib/gateway/apply.ts): "This checkout's Gateway (…) is older than the live one (…)". Someone deployed from a newer checkout:git pull && pnpm install, then the same command. Fromsetupandupdatethe error'sdata.rerunadds--allow-downgradewithdata.authorize: true; use it only for a rollback the person asked for.CLI_TOO_OLD: the release needs a newer everyapp;git pull.- An app:
pnpm everyapp deploy --jsonin its directory. Its data stays; a change to what it stores needs a migration (pnpm db:generate) or the deploy stops withSCHEMA_DRIFTatapp.schema. - After either,
pnpm everyapp status --json.
Verification checklist
pnpm everyapp status --json reads the Gateway's D1 and cf, asks nothing of the Gateway over HTTPS, and ends with one done event (packages/cli/src/commands/status/index.ts, problems.ts). Its message is "Gateway https://…: N apps live, nothing to fix" or "… M problems to fix". Its data.next says what isn't done yet (next.ts): at most four { code, command?, url?, note, why }, an AI key, an MCP app to turn on, Cloudflare Access, people to invite, an AI client to connect, the first app, email, each from the Gateway's state and gone once it changes, never repeating a fix (the codes and the reads on /docs/agent/json). After the fixes are done, tell the person each step's why and offer its command or url. No Gateway in any account the login sees (a setup that stopped before the database was created): status stops with action_required install_gateway (exit 2), "No Gateway in account's Cloudflare account (logged in as …). Gateway in another account? Log in to it: pnpm everyapp login", data.rerun pnpm everyapp setup ("installs one"), data.accounts (existingGateway in packages/cli/src/lib/gateway/connect.ts). That rerun carries no --domain: add --domain <host> before running it under --json, or DOMAIN_REQUIRED follows.
After setup
done.data should read:
| Field | Expected |
|---|---|
installed |
true. false (with deployed: true) means the database exists but the install row was never written: fix GATEWAY_NOT_INSTALLED, run pnpm everyapp setup. |
gatewayUrl, appBaseDomain |
https://<host> and the host apps go under. |
mode |
self_hosted (what setup installs: GATEWAY_DEPLOYMENT_MODE, set with the first upload's secrets and kept by every later one, packages/cli/src/lib/gateway/apply.ts) or hosted (EveryApp's own install of the same Gateway, apps/every-app-gateway/src/server/platform-policy.ts); null before the first upload. |
release.version, release.sha256 |
The live release. |
versionId = recordedVersionId |
The Worker serves the version setup uploaded. Different: fix GATEWAY_CHANGED (setup redeploys, asking first). |
owner, claimed |
{ email, organizationName } and true. null and false: fix NO_OWNER, run setup for a new link. |
tls |
true once the Gateway host has its certificate; null when cf couldn't list certificates (CERTIFICATES_UNKNOWN). certificates[] has { host, ready } per hostname; CERT_PENDING settles by itself. |
inSync |
true: the live Gateway binds exactly desiredApps and publishes exactly domains. false: fix BINDINGS_DIFFER, run setup. |
upToDate, checkout |
true when the live release was built from this checkout's Gateway sources (by tree hash); null when unknown (a release not built from a checkout); false: GATEWAY_OUTDATED (info; pnpm everyapp update updates it) or CHECKOUT_BEHIND (git pull). |
leases |
[]; an entry is a run in progress (DEPLOY_RUNNING, settles). |
access |
{ on, teamDomain } (accessStateOf in packages/cli/src/lib/access/settings.ts, from the Worker's secret names and the D1 record; teamDomain is null when nothing recorded it), or null for a hosted Gateway, before the first upload, or when cf couldn't say (then warning ACCESS_UNKNOWN, "Couldn't check whether Cloudflare Access is on", detail "Cloudflare said: …"). |
fixes |
[]. Each entry is { code, severity, message, run, note?, fix? } (fixData in index.ts); severity is error, warning, info or settling, and only error and warning count as problems in message (needsFixing in problems.ts); run is the command to execute, null when there is none (the remedy is then in fix, in words, or nothing is to be done); note says what run does ("updates it now"). |
next |
On a fresh Gateway CREATE_APP (pnpm everyapp create my-app); once the owner has claimed, ACCESS_OFF (pnpm everyapp access on, self-hosted only) and, with an app live, INVITE (https://<gateway>/admin/users, until someone is invited), CONNECT_AI (https://everyapp.com/docs/connect-ai, the note naming https://<gateway>/mcp), AI_KEY, PROVIDER_OFFER and EMAIL_UNSET as they apply, four at most. Each goes when its state changes; none repeats a fix. |
Then open https://<gateway>/ (the sign-in page, or the owner's session) and https://<gateway>/admin. A fresh hostname can take a few minutes to answer over HTTPS.
After a deploy
apps[]has the app:status: "active",bound: true, itshostname,deployedBy(the login's email, orapi-token:<account>),publicRoutes(the PUBLIC column counts them) andsetup(nullwhen the manifest declares nothing to set; else{ ready, settings, unmet, groups, command }:ready: falsemeans the app shows Needs setup and doesn't open untilcommand,pnpm everyapp configure <app>, has run).status: "binding"withbound: trueisAPP_BINDING: it goes live on its first visit, so open it.status: "pending"isAPP_NOT_UPLOADED: registered, but no deploy finished.domainsandliveDomainsboth include the app's hostname;inSync: true.certificates[]shows the hostnameready, orCERT_PENDINGfor a few minutes.- Open
https://<app>.<base>/: the Gateway's sign-in, or the app for a signed-in person. - What the Gateway UI shows (
apps/every-app-gateway/src/client/lib/appStatus.ts): Needs deploy means no hostname orstatus: "pending"(the app isn't online:pnpm everyapp deploy --jsonin its directory); Deploying meansbinding(open it once; it goes live on the first visit); Disabled means an admin switched it off; Needs setup means it is bound and active but a required setting is unset (status --jsondata.apps[].setup.unmet; the setup dialog opens in its place and its tools stay out of/mcp:pnpm everyapp configure <app>); Active opens, except for an MCP app (pnpm everyapp mcp on; tierprovider, an "MCP" chip beside its name, listed under "MCP apps"), which never opens: its tools are on/mcp, and its card opens the "Use it from your agent" dialog.
Fix codes status can report, with what run says:
| Code | Run |
|---|---|
CHECKOUT_BEHIND |
git pull |
NO_OWNER, GATEWAY_NOT_INSTALLED, GATEWAY_NOT_UPLOADED, GATEWAY_CHANGED, BINDINGS_DIFFER |
pnpm everyapp setup |
GATEWAY_OUTDATED |
pnpm everyapp update (note "updates it now"). severity info: listed last, not a problem in message; the next new app's deploy (or a delete) that rebinds the Gateway updates it too. |
APP_WORKER_MISSING, APP_NOT_UPLOADED |
pnpm everyapp deploy from the app's directory, or pnpm everyapp delete <id> |
APP_NOT_BOUND |
pnpm everyapp delete <id> |
EMAIL_ROUTE_PENDING, EMAIL_ROUTE_MISSING |
pnpm everyapp deploy from the app's directory |
EMAIL_INBOUND_UNDECLARED |
run is null; fix says: declare it (email: { inbound: true }) in everyapp.config.ts and pnpm everyapp deploy, or drop the addresses with pnpm everyapp email unset inbound, either from the app's directory |
CERTIFICATES_UNKNOWN, EMAIL_ROUTES_UNKNOWN, ACCESS_UNKNOWN |
run is null: cf couldn't list the zone's certificates, read the apps' Email Routing rules, or read the Gateway Worker's Access settings (message quotes "Cloudflare said: …"). Warnings, so they count in message; tls and certificates are null with the first, access with the last. Run status again. |
APP_NEEDS_SETUP |
pnpm everyapp configure <app> (the app's setup.command; the detail names each unmet setting and its guide). A warning, so it counts in message. |
SETTINGS_UNKNOWN |
run is null: cf couldn't list a Worker's secrets (message quotes "Cloudflare said: …"), so the settings it holds read set: null in apps[].setup, never blocking. Run status again. |
APP_BINDING, CERT_PENDING, DEPLOY_RUNNING |
Nothing: they settle by themselves and don't count as problems in message. |
Recovery
| Situation | What you see | What to do |
|---|---|---|
| Setup stopped on a handoff | Exit 2 and an action_required. |
Hand off (table above); when the person is done, run data.rerun with --json. |
| Setup stopped before the first upload | status: GATEWAY_NOT_UPLOADED ("Setup finishes it, then prints the link…") or GATEWAY_NOT_INSTALLED ("asks for the hostname again"); app commands: GATEWAY_NOT_INSTALLED. |
pnpm everyapp setup --domain <host> --json; it resumes. |
| Owner link lost, expired or replaced | claim_owner "Use the newer setup link, or get a new one"; status NO_OWNER. |
Run setup again: it mints a new link and the old one stops working. Only the person opens it. Whoever opens a link first becomes the owner; nothing in the CLI undoes a claim. |
| The person opened the link after the run ended | The run exited 2 with claim_owner and data.setupUrl. |
Run setup again: setup.owner skip "Owner account is …" and done. |
DOWNGRADE_REFUSED |
"This checkout's Gateway (…) is older than the live one (…)", data.live, data.candidate. |
git pull && pnpm install, same command. --allow-downgrade (data.authorize) only for a rollback the person asked for. |
LEASE_HELD |
"… is still running after 5m. It's from deployer on machine. If it stopped without cleaning up, it's released within …" (data.holder); retryable. |
pnpm everyapp status --json shows the holder under leases. A dead holder is released within 3 minutes (the lease TTL, packages/cli/src/lib/gateway/lease.ts). Rerun once it is gone. |
DNS_RECORD_CONFLICT |
"… already points somewhere else. Its DNS record: …", data.authorize: true. |
Ask the person to delete the record (their domain → DNS in the Cloudflare dashboard) or choose another name; --force-dns only on their word. |
| Worker changed outside everyapp | rerun_interactive, or status fix GATEWAY_CHANGED. |
The person runs data.rerun at their own terminal and confirms cf's prompt. |
DOMAIN_CHANGE_UNSUPPORTED |
"The Gateway can't move to host", "It's installed at gatewayHost, and moving an installed Gateway isn't supported." |
Run data.rerun (the command without --domain, data.rerunNote "(without --domain) to update it where it is"). Tell the person the Gateway can't move. |
GATEWAY_UNREACHABLE warning before the link |
data.kind tls (certificate on its way), dns (no address yet) or other. |
Nothing: the link works as soon as the host answers, usually within minutes. |
CLOUDFLARE_LOGIN_FAILED |
The device login wasn't approved in 5 minutes, or didn't complete. | pnpm everyapp login --json and hand off the new link and code. |
UNEXPECTED |
"everyapp hit an unexpected error … This is a bug in everyapp". | Read data.stack from the event (it is there whenever the thrown error has a stack, with or without --verbose; the suggested --verbose rerun, "for a stack trace to report", is only added to a non-verbose run so a person's terminal prints the frames; packages/cli/src/lib/output/report.ts) and report it. |
Codes and messages in full: /docs/agent/troubleshooting and /docs/agent/errors; the person-facing version is /docs/troubleshooting.
Safety rules for agents
From packages/cli/skills/everyapp/SKILL.md:
- Always
pnpm everyappfrom the checkout; nevernpx everyapp. Always--json, except fordev, which is long-running and human-facing: run it in the background and read its URL. - Exit 2: show the person the last
action_required(message,data.url,data.code), then run itsdata.rerun. Exit 1: show the error; ifdata.authorizeis set, ask before runningdata.rerun.data.waiting: true: keep reading (5 minutes for the Cloudflare login, up to 15 for the owner link). - Never add
--yes,--purge,--force-dns,--allow-delete-durable-object-data,--allow-delete-workflows,--enable-email-routingor--allow-downgradewithout the person's word, and never todeleteunless they asked. - Never print, log or ask in chat for a secret value: not the Cloudflare API token for browser installs, not an AI key, not
POSTGRES_DATABASE_URLfrom.everyapp/secrets.env. Values travel through--from-env, stdin, or a hidden prompt the person answers; never the command line. - Only a person approves the Cloudflare login, opens the owner link, picks a workers.dev subdomain, pays for Workers Paid, buys a domain, enters the browser-install token, picks the Access team name, picks the account (
choose_account: onererunper choice) and confirms at a terminal a Worker changed outside everyapp (rerun_interactive, which like theOWNER_NOT_CLAIMEDwarning carriesdata.needsTerminal: true: hand overdata.rerunas given, never with--json). pnpm everyapp db restore --json(undoes the last deploy's migrations and every write since) only when the person asks. A productiondb querywrite stops withCONFIRMATION_REQUIRED; add--yesonly if they said so.- Never build a repository from its URL yourself;
install <url>takes its published release. pnpm everyapp teardown --account-id <id> --dry-run --jsondeletes everything EveryApp made in one Cloudflare account, the Gateway included: only a dedicated test account the person names (EVERYAPP_TEST_ACCOUNTS, aCLOUDFLARE_API_TOKENscoped to it),--dry-runfirst and showdata.plan, and never--confirm <id>unless they explicitly asked (TEARDOWN_CONFIRMATION_REQUIRED,authorize: true).
Sources
packages/cli/skills/everyapp/SKILL.mdlanding-page/content/docs/gateway.mdxlanding-page/content/docs/cloudflare.mdxlanding-page/content/docs/domain.mdxlanding-page/content/docs/create-an-app.mdxlanding-page/content/docs/update.mdxlanding-page/content/docs/people.mdxlanding-page/content/docs/troubleshooting.mdxpackages/cli/src/commands/setup/command.tspackages/cli/src/commands/setup/index.tspackages/cli/src/commands/setup/runSetup.tspackages/cli/src/commands/setup/deployGateway.tspackages/cli/src/commands/login/command.tspackages/cli/src/commands/login/index.tspackages/cli/src/lib/cf/account.tspackages/cli/src/lib/cf/deploy.tspackages/cli/src/lib/cf/plan.tspackages/cli/src/lib/gateway/setup-link.tspackages/cli/src/lib/gateway/discover.tspackages/cli/src/lib/gateway/connect.tspackages/cli/src/lib/gateway/install.tspackages/cli/src/lib/gateway/apply.tspackages/cli/src/lib/gateway/lease.tspackages/cli/src/lib/gateway/release.tspackages/cli/src/lib/gateway/checkoutRelease.tspackages/cli/src/lib/gateway/change.tspackages/cli/src/lib/domains/preflight.tspackages/cli/src/lib/domains/certWait.tspackages/cli/src/commands/create/command.tspackages/cli/src/commands/create/index.tspackages/cli/src/commands/create/steps/appId.tspackages/cli/src/lib/app/skill.tspackages/cli/src/commands/deploy/command.tspackages/cli/src/commands/deploy/index.tspackages/cli/src/commands/deploy/deployApp.tspackages/cli/src/commands/deploy/steps/offerGateway.tspackages/cli/src/commands/deploy/steps/waitForLive.tspackages/cli/src/commands/deploy/steps/checkSchemaDrift.tspackages/cli/src/commands/appGateway.tspackages/cli/src/commands/dev/command.tspackages/cli/src/commands/status/command.tspackages/cli/src/commands/status/index.tspackages/cli/src/commands/status/next.tspackages/cli/src/commands/status/problems.tspackages/cli/src/commands/status/render.tspackages/cli/src/commands/update/command.tspackages/cli/src/commands/update/index.tspackages/cli/src/commands/secret/command.tspackages/cli/src/commands/secret/index.tspackages/cli/src/lib/secretValue.tspackages/cli/src/commands/flags.tspackages/cli/src/lib/errors.tspackages/cli/src/lib/output/events.tspackages/cli/src/lib/output/run.tspackages/cli/src/lib/output/report.tspackages/cli/src/lib/output/command.tspackages/cli/src/lib/output/prompt.tspackages/cli/src/lib/checkout.tspackages/cli/src/lib/runtime.tspackages/cli/src/app.tspackages/cli/bin/everyapp.jspackages/cli/package.jsonpackage.jsonREADME.mddocs/runbooks/browser-app-install.mddocs/runbooks/cloudflare-access.mdapps/every-app-gateway/src/routes/setup.tsxapps/every-app-gateway/src/client/lib/appStatus.tsapps/every-app-gateway/src/server/platform-policy.tspackages/cli/src/commands/access/command.tspackages/cli/src/commands/access/index.tspackages/cli/src/lib/access/plan.tspackages/cli/src/lib/access/record.tspackages/cli/src/lib/access/sync.tspackages/cli/src/lib/access/settings.tspackages/cli/src/commands/delete/index.tspackages/cli/src/commands/dev/gatewayServer.tspackages/cli/src/argumentErrors.tspackages/cli/src/lib/output/time.tspackages/sdk/src/internal/email.tsstarter/package.jsonlanding-page/content/docs/access.mdxlanding-page/content/docs/connect-ai.mdx
Docs for coding agents
How a coding agent uses these docs to build, deploy and operate EveryApp apps for a person, and which page answers which question.
The open apps
The apps EveryApp publishes, for an agent installing one for a person. Each app's manifest id, repository, install command, every setting it declares (required or optional, held by the Gateway or by the app), its public routes, who can open it, and whether it needs R2.