EveryAppEveryApp

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": "…", … }; type is step, progress, warning, action_required, error or done. Nothing is ever prompted under --json.
  • Exit codes: 0 after done; 1 after an error, whatever came before it; 2 when the run stopped on an action_required; 251 for a command it can't find (error UNKNOWN_COMMAND) and 252 for bad arguments (UNKNOWN_FLAG, INVALID_VALUE, MISSING_ARGUMENT, …), whose data carries help (the --help command to run), usage or didYouMean (packages/cli/src/argumentErrors.ts).
  • action_required has action (one of cloudflare_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.rerun and data.rerunNote, or data.choices[] (each with rerun and label) when the way on is one of several commands.
  • data.waiting: true means the run waits for the person (a login to approve, the owner link): show url and code, keep reading. If the person acts in time the run goes on to done (exit 0) or an error (exit 1); if not, a last action_required ends it (exit 2).
  • error has a stable code, retryable, data.fix (a remedy in words), data.rerun with data.rerunNote, and data.authorize: true when 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.rerun is the same command, --json included when this run had it (thisCommand in packages/cli/src/lib/output/command.ts): run it as it is. The exception is data.needsTerminal: true (ActionData in events.ts: "Only a person at a terminal can do the rerun"), which both rerun_interactive and the OWNER_NOT_CLAIMED warning carry; those reruns are built without --json (thisCommand([], { terminal: true })), and their data.rerunNote is "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 with pnpm everyapp because pnpm started the CLI.
  • A warning is emitted once per run; done carries data.durationMs, and for setup and deploy data.facts and data.next.
  • "Interactive" means canPromptInTerminal (packages/cli/src/lib/output/prompt.ts): stdin and stderr are TTYs, no --json, and no CI environment variable; piping stdout changes nothing. Without --json and without a terminal on stdin and stderr (an agent's shell, CI), setup stops at cloudflare_login with no device code ("Log in to Cloudflare with pnpm everyapp login. Unattended? Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID instead."): only --json hands cf's device link and code over (onDeviceLogin is wired when flags.json in packages/cli/src/commands/setup/runSetup.ts; logIn in packages/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.com puts an app at todo.apps.example.com.
  • Under --json, a new install needs --domain: without it setup fails with DOMAIN_REQUIRED ("Choose the Gateway's address with --domain", data.zones lists the account's active domains). Once the install row exists --domain is 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: --domain naming another host fails with DOMAIN_CHANGE_UNSUPPORTED, whose data.rerun is the command without --domain.
  • A zone apex (--domain example.com) is allowed with the warning GATEWAY_AT_ZONE_APEX: the sign-in cookie covers every subdomain and apps take names directly under the domain; data.suggestedDomain is apps.example.com.
  • A --domain that isn't a full hostname is refused before anything is asked of Cloudflare.

Step 2: run setup

pnpm everyapp setup --domain apps.example.com --json

Flags (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"); --domain is 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 no DNS_RECORD_CONFLICT and no terminal. When the row did record the version but not the hostname, the rerun is change: "resume" too, start "Finishing the Gateway's first publication at host (release)" and ok "The Gateway (release) is live at url", data.gateway: "installed" in done; repair is 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.gateway skip and data.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).

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 --json

What 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: error ACCESS_NOBODY_ALLOWED ("Nobody to let in yet … Claim the Gateway first, or name who will (the setup link is then behind Access too)."), data.rerun pnpm 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 --email with no account on the Gateway yet: warning ACCESS_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: error ACCESS_TEAM_REQUIRED with data.suggested (the zone's first label). The team name is permanent: its sign-in page is <name>.cloudflareaccess.com for 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 with ACCESS_TEAM_CREATE_FAILED: ask for another.
  • A hosted Gateway: error ACCESS_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) and first (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_MEMBER warns 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.signInMethods lists the providers; done.data.identityProviders says which of the two this run created. done.data.next lists pnpm 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 on again after the person invites someone, so Access lets them in. access show --json reports without changing anything: data.on, and data.fixes lists pnpm everyapp access on when something differs from what on would make. access off asks first ("Turn Cloudflare Access off for host?"); under --json without --yes it is error CONFIRMATION_REQUIRED ("Turning Access off needs confirmation at a terminal, or --yes") with --yes in data.rerun and data.authorize: true: add --yes only on the person's word.
  • Behind Access, setup warns ACCESS_IN_FRONT before 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 make live (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.rerun status) and skip app.live.
  • After app.live, every deploy (and delete) keeps the apps application in step when one exists (syncAppsAccessAfter in packages/cli/src/lib/access/sync.ts): step app.access ok "Cloudflare Access lets appId's <public routes | /mcp | public routes and /mcp> through." or skip "Cloudflare Access already matched appId's <…>." (data.changed, data.removed; the words follow what the manifest opens: its public routes, its /mcp where it sets mcp, or both). A login that can't read Access warns ACCESS_NOT_CHECKED (only when the app has public routes or sets mcp), a failed update ACCESS_SYNC_FAILED, both with data.rerun pnpm 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> --json

From 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: its package.json takes the SDK and the CLI by link: path, so neither folder moves. For someone with many apps, a folder of apps inside the checkout also works: a bare pnpm everyapp create <name> from anywhere in the checkout makes apps/<name>, untracked by git, no repository of its own.
  • The argument is a path when it has a slash or starts with ., ~ or / (isPathArgument in steps/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> from packages/cli) is the inside rule; any other place inside the checkout is refused, APP_DIR_IN_CHECKOUT, with a data.fix naming the two places it can go. A bare name goes in apps/<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 is error INVALID_APP_ID with a data.fix that suggests a slug ("Analytics_App" becomes analytics-app; for a path, its last part). Under --json the 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>, and AGENTS.md pointing 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 is INSTALL_FAILED with rerun cd <dir> && pnpm install: the app exists, only the install is left), create.types (warning WORKER_TYPES_FAILED; pnpm dev tries again), migrations.local (warning MIGRATIONS_FAILED), and outside the checkout create.git (warning GIT_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's package.json scripts dev and deploy run everyapp dev and everyapp deploy, starter/package.json). Nothing is deployed: the deploy is the next command, from the app's directory (Deploy).
  • --yes asks nothing (a name is then required). --verbose streams 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 dev

From 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 --json

From 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_required install_gateway (exit 2): "Install a Gateway first: account has none yet. Apps deploy behind one. Setup needs a person once, to create the owner account." data.rerun is pnpm everyapp setup --domain apps.<zone> (the account's first active zone, or the placeholder <hostname> when it can't tell), with data.accounts and data.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; --yes installs there without asking; declining is CANCELLED (exit 1, nothing deployed).
  • Login under --json: cloudflare_login on step app.login, as in setup.
  • Which Gateway: the one the login finds in its accounts; its URL always comes from the install row. --gateway <url> (or EVERYAPP_GATEWAY_URL) and --account-id <id> only pick among several (requestedGatewayUrl and connectToGateway in packages/cli/src/lib/gateway/connect.ts). A URL whose host isn't the Gateway found: error GATEWAY_URL_MISMATCH ("The Gateway here is found, not host", "For host, pass its account with --account-id"), with data.rerun the command without --gateway, or when the URL came from the environment the fix "unset EVERYAPP_GATEWAY_URL to use found" (checkGatewayUrl in packages/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 same DNS_RECORD_CONFLICT, DNS_UNREADABLE and ZONE_CUSTOM_DOMAIN_LIMIT as setup, --force-dns the takeover, and the warnings CUSTOM_DOMAIN_OWNER_UNCHECKED and ZONE_CUSTOM_DOMAIN_LIMIT_NEAR as in setup's domain row; preflightAppHost in packages/cli/src/lib/domains/preflight.ts), app.resources (D1, KV and Hyperdrive created or found, progress "Created …"; warning RESOURCE_RECREATED means 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: error SCHEMA_DRIFT, headline "The schema has changes no migration covers", data.rerun pnpm db:generate with data.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 --json undoes it, only when the person asks), app.upload, then for a new app or a new hostname app.bind, with gateway.upgrade ("Upgrading the Gateway from → to") nested inside it, between app.bind start and ok, when the checkout's Gateway version differs from the live one (an older one is refused: DOWNGRADE_REFUSED), then app.live, then app.access when Cloudflare Access fronts the apps (above). A redeploy of an app the Gateway already binds has no app.bind: it goes from app.upload to app.live (bindIfNeeded in packages/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: true and the flag in data.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_REFUSED when the checkout is behind the live Gateway: git pull, same command. rerun_interactive when the app's Worker was changed outside everyapp.
  • app.live (packages/cli/src/commands/deploy/steps/waitForLive.ts, awaitLive in appGateway.ts): the deploy waits up to 10 minutes for the registry to say active, requesting https://<hostname>/ every 3 s, with a progress line every 15 s (data.waitedMs, data.answered). Live: step ok with data.url and, when the wait showed, data.waited (certificate or live). Not live: a warning says why, then step skip with no data.reason: CERT_PENDING (no answer in 10 minutes; "goes live on its first visit once it has one", data.hostname, data.fix about 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.rerun pnpm everyapp status), or APP_DISABLED (switched off in Admin → Apps). The Gateway answered but the app never went active in 10 minutes: error APP_NOT_LIVE, retryable, data.rerun pnpm everyapp status "then deploy again". With --no-wait there is no wait: ok at once for an app that was live before this redeploy, otherwise skip with data.reason certificate (a new hostname) or first-visit (a hostname already published). data.reason outside-base (the hostname isn't one label below the app base domain; warning APP_HOST_OUTSIDE_BASE, never routed, never waited for). Warning LOCAL_DNS_STALE: live, but this computer's resolver has no address for it yet.
  • After the upload, step app.secrets warns APP_NEEDS_SETUP ("<app> won't open until <names> is set", the first unmet setting's why under it, data.setup) when a required setting is unset, and next gets 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> --json deploys its latest release's everyapp-release.tar.gz. NO_RELEASE: clone it, pnpm install in it, then this checkout's pnpm everyapp deploy from 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> from app.live ok (data.waited: "certificate" adds Certificate issued above it). Not live yet: URL …, plus a Status line only when the skip carries data.reason (a --no-wait run): Goes live on its first visit, once its certificate comes (a few minutes) for certificate, Goes live on its first visit for first-visit; outside-base prints Live Not routed. After a waited run that stopped on CERT_PENDING, ACCESS_IN_FRONT or APP_DISABLED, the skip has no reason: tell the person what that warning said.
  • Who can open … · change it at https://<gateway>/admin/apps from app.register ok on a first deploy (data.firstDeploy: true): data.scope: "shared" → "Members of every organization (shared app)"; data.defaultAccess: true → "Everyone in organizationName"; data.deployerAccess: false → "Private to owners and admins"; otherwise "Private to you, owners and admins". data.hasOwner: false adds "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 /collect from app.register ok data.publicRoutes (also on done.data): the manifest's public routes, the first six, then … and N more (everyapp.config.ts public); nothing when the manifest has no public. 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_SETUP means 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 /mcp until it is. done.data.next carries 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 pipes NAME=value lines into it or adds --from-env (AI keys and app secrets); it ends with <app> is ready, and pnpm everyapp secret list <app> --json shows data.app.setup.ready: true.
  • Then pnpm everyapp status --json: its data.next says 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 a why to 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 --json

From 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 is pnpm everyapp secret set NAME [--app <id>] (pnpm everyapp secret set OPENAI_API_KEY --from-env OPENAI_API_KEY --json for an agent). Reference: /docs/agent/secrets.
  • OPENAI_API_KEY (and OPENROUTER_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 index setup/README.md lists them all; in an app under .claude/skills/everyapp/setup/). DATAFORSEO_API_KEY can also back an MCP app: pnpm everyapp mcp on dataforseo --json offers DataForSEO's tools to members' AI clients at /mcp with the Gateway's key, nothing to deploy; it shows Needs setup until the key is set (PROVIDER_KEY_UNSET names it). mcp list --json says which are on and whether each key is set. Reference: /docs/agent/mcp.
  • A value never comes from the command line (configure NAME=value is 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_ARGUMENTS or INVALID_VALUE, with the value cut out or never shown and the line A value never goes on the command line: it lands in the shell's history and the process list. Rotate it if it was real.; nothing is set). Under --json there is no prompt: configure <app> takes NAME=value lines on stdin (one per line; blank lines and # lines skipped, an export prefix allowed, one pair of quotes around a value dropped, the value otherwise as it is; a line that isn't NAME=value is PIPED_LINE_INVALID and never echoed, since a bare value piped in may be a secret; a name the manifest doesn't declare is SECRET_NOT_DECLARED, echoed only when it looks like a setting name; a blank value SECRET_EMPTY; each refused before anything is written) or, with --from-env, each unset setting from the environment variable of its own name (DATAFORSEO_API_KEY from $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 NAME takes --from-env VAR or one piped value. With nothing to read and a required setting unset: error SECRET_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.names the names still missing, data.rerun pnpm everyapp configure <app> --from-env once deployed, pnpm everyapp configure --from-env in the app's directory before its first deploy; from secret set: "Can't ask for …'s value without a terminal", data.rerun pnpm 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 under secrets in its everyapp.config.ts and is set after its first deploy: configure <app> from anywhere, or secret set from the app's directory or from anywhere with --app <id> (against the manifest its last deploy stored). Before that deploy, configure in the app's directory sets the Gateway's settings and leaves the app's own for after it (next pnpm everyapp deploy, then pnpm everyapp configure <id> for them; while a Gateway setting is still missing, next is pnpm everyapp configure with note in the app's directory: …; a piped own secret is APP_NOT_FOUND, "id isn't deployed yet. Its own NAME can be set after its first deploy. Nothing was set."), and configure <id> by name is APP_NOT_FOUND ("id isn't deployed to this Gateway", data.rerun pnpm everyapp status; a row whose first deploy never finished, status: "pending", counts as undeployed). From secret set before that deploy: error APP_NOT_FOUND ("id isn't deployed yet. Its secrets can be set after its first deploy. Nothing was changed.", data.rerun pnpm everyapp deploy). A name the manifest doesn't list: SECRET_NOT_DECLARED, whose data.fix quotes the secrets: […] line to add and whose data.rerun is pnpm everyapp secret set <name>; no everyapp.config.ts in the directory and no --app: SECRET_NOT_DECLARED with fix "cd into the app's directory and run this again, or add --app "; --app naming an app that isn't deployed: APP_NOT_FOUND ("id isn't deployed to this Gateway") with data.rerun pnpm everyapp status; a Gateway setting or declared name typed in another case or without its underscores: SECRET_NOT_DECLARED with data.didYouMean and a corrected data.rerun. Later deploys keep it. For dev, 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.setup has ready, unmet (each with names, holder, need.kind, why, docs, setup, the guide file) and command, { command: "pnpm everyapp configure <app>" }, the one command that opens the app (null once it is ready). pnpm everyapp status --json carries the same per app as data.apps[].setup, and lists APP_NEEDS_SETUP in fixes. Reference: /docs/agent/secrets, the shape on /docs/agent/json.

Keeping things up to date

  • The Gateway, in the checkout: git pull && pnpm install, then pnpm everyapp update --json (packages/cli/src/commands/update/command.ts; setup --json does 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_FOUND with rerun pnpm everyapp setup --domain <hostname>; an install that never uploaded is GATEWAY_NOT_INSTALLED. done.message "Upgraded to releaseName" and data.gateway: "upgraded"; already current: setup.gateway skip and data.gateway: "up-to-date". On a live self-hosted Gateway behind Cloudflare Access, update (and setup) then brings both Access applications in line with what this release lets through (its bypass paths, the sign-in methods access on vouches for, the policy), under the access lease, as step gateway.access (ok when one changed, skip when 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, so access off stays off; a failure there is the warning ACCESS_RECONCILE_FAILED (or ACCESS_NOT_CHECKED when the applications couldn't be listed) with data.rerun pnpm everyapp access on, never a failed update; an application deleted by hand is ACCESS_APPLICATION_MISSING (nothing is created: that is access on's job). The Gateway's three settings aren't rewritten by update: an AUD tag that changed is access 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.upgrade inside app.bind). A redeploy of an app the Gateway already binds never touches it, however new the checkout is; status then lists GATEWAY_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.") as severity: "info", with run pnpm everyapp update and note "updates it now"; it comes last in fixes and doesn't count in message (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. From setup and update the error's data.rerun adds --allow-downgrade with data.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 --json in its directory. Its data stays; a change to what it stores needs a migration (pnpm db:generate) or the deploy stops with SCHEMA_DRIFT at app.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, its hostname, deployedBy (the login's email, or api-token:<account>), publicRoutes (the PUBLIC column counts them) and setup (null when the manifest declares nothing to set; else { ready, settings, unmet, groups, command }: ready: false means the app shows Needs setup and doesn't open until command, pnpm everyapp configure <app>, has run). status: "binding" with bound: true is APP_BINDING: it goes live on its first visit, so open it. status: "pending" is APP_NOT_UPLOADED: registered, but no deploy finished.
  • domains and liveDomains both include the app's hostname; inSync: true.
  • certificates[] shows the hostname ready, or CERT_PENDING for 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 or status: "pending" (the app isn't online: pnpm everyapp deploy --json in its directory); Deploying means binding (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 --json data.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; tier provider, 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 everyapp from the checkout; never npx everyapp. Always --json, except for dev, 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 its data.rerun. Exit 1: show the error; if data.authorize is set, ask before running data.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-routing or --allow-downgrade without the person's word, and never to delete unless 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_URL from .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: one rerun per choice) and confirms at a terminal a Worker changed outside everyapp (rerun_interactive, which like the OWNER_NOT_CLAIMED warning carries data.needsTerminal: true: hand over data.rerun as 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 production db query write stops with CONFIRMATION_REQUIRED; add --yes only 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 --json deletes everything EveryApp made in one Cloudflare account, the Gateway included: only a dedicated test account the person names (EVERYAPP_TEST_ACCOUNTS, a CLOUDFLARE_API_TOKEN scoped to it), --dry-run first and show data.plan, and never --confirm <id> unless they explicitly asked (TEARDOWN_CONFIRMATION_REQUIRED, authorize: true).

Sources

  • packages/cli/skills/everyapp/SKILL.md
  • landing-page/content/docs/gateway.mdx
  • landing-page/content/docs/cloudflare.mdx
  • landing-page/content/docs/domain.mdx
  • landing-page/content/docs/create-an-app.mdx
  • landing-page/content/docs/update.mdx
  • landing-page/content/docs/people.mdx
  • landing-page/content/docs/troubleshooting.mdx
  • packages/cli/src/commands/setup/command.ts
  • packages/cli/src/commands/setup/index.ts
  • packages/cli/src/commands/setup/runSetup.ts
  • packages/cli/src/commands/setup/deployGateway.ts
  • packages/cli/src/commands/login/command.ts
  • packages/cli/src/commands/login/index.ts
  • packages/cli/src/lib/cf/account.ts
  • packages/cli/src/lib/cf/deploy.ts
  • packages/cli/src/lib/cf/plan.ts
  • packages/cli/src/lib/gateway/setup-link.ts
  • packages/cli/src/lib/gateway/discover.ts
  • packages/cli/src/lib/gateway/connect.ts
  • packages/cli/src/lib/gateway/install.ts
  • packages/cli/src/lib/gateway/apply.ts
  • packages/cli/src/lib/gateway/lease.ts
  • packages/cli/src/lib/gateway/release.ts
  • packages/cli/src/lib/gateway/checkoutRelease.ts
  • packages/cli/src/lib/gateway/change.ts
  • packages/cli/src/lib/domains/preflight.ts
  • packages/cli/src/lib/domains/certWait.ts
  • packages/cli/src/commands/create/command.ts
  • packages/cli/src/commands/create/index.ts
  • packages/cli/src/commands/create/steps/appId.ts
  • packages/cli/src/lib/app/skill.ts
  • packages/cli/src/commands/deploy/command.ts
  • packages/cli/src/commands/deploy/index.ts
  • packages/cli/src/commands/deploy/deployApp.ts
  • packages/cli/src/commands/deploy/steps/offerGateway.ts
  • packages/cli/src/commands/deploy/steps/waitForLive.ts
  • packages/cli/src/commands/deploy/steps/checkSchemaDrift.ts
  • packages/cli/src/commands/appGateway.ts
  • packages/cli/src/commands/dev/command.ts
  • packages/cli/src/commands/status/command.ts
  • packages/cli/src/commands/status/index.ts
  • packages/cli/src/commands/status/next.ts
  • packages/cli/src/commands/status/problems.ts
  • packages/cli/src/commands/status/render.ts
  • packages/cli/src/commands/update/command.ts
  • packages/cli/src/commands/update/index.ts
  • packages/cli/src/commands/secret/command.ts
  • packages/cli/src/commands/secret/index.ts
  • packages/cli/src/lib/secretValue.ts
  • packages/cli/src/commands/flags.ts
  • packages/cli/src/lib/errors.ts
  • packages/cli/src/lib/output/events.ts
  • packages/cli/src/lib/output/run.ts
  • packages/cli/src/lib/output/report.ts
  • packages/cli/src/lib/output/command.ts
  • packages/cli/src/lib/output/prompt.ts
  • packages/cli/src/lib/checkout.ts
  • packages/cli/src/lib/runtime.ts
  • packages/cli/src/app.ts
  • packages/cli/bin/everyapp.js
  • packages/cli/package.json
  • package.json
  • README.md
  • docs/runbooks/browser-app-install.md
  • docs/runbooks/cloudflare-access.md
  • apps/every-app-gateway/src/routes/setup.tsx
  • apps/every-app-gateway/src/client/lib/appStatus.ts
  • apps/every-app-gateway/src/server/platform-policy.ts
  • packages/cli/src/commands/access/command.ts
  • packages/cli/src/commands/access/index.ts
  • packages/cli/src/lib/access/plan.ts
  • packages/cli/src/lib/access/record.ts
  • packages/cli/src/lib/access/sync.ts
  • packages/cli/src/lib/access/settings.ts
  • packages/cli/src/commands/delete/index.ts
  • packages/cli/src/commands/dev/gatewayServer.ts
  • packages/cli/src/argumentErrors.ts
  • packages/cli/src/lib/output/time.ts
  • packages/sdk/src/internal/email.ts
  • starter/package.json
  • landing-page/content/docs/access.mdx
  • landing-page/content/docs/connect-ai.mdx

On this page