Hosted Install
Operator runbook (hosted tier, post-launch): the hosted Gateway (app.everyapp.com): install, shared first-party apps, the gated third-party deploy API and its deploy keys, launch checks.
Mirrored from
docs/runbooks/hosted-install.mdin the EveryApp repository, where it is edited.Read with care: Written for the 2026-10-16 hosted build. The hosted tier is not open to third-party deploys at launch, and the CLI surface this runbook drives was removed on 2026-10-24 (its first paragraph says what).
The hosted tier is post-launch. The CLI's hosted-only surface (
setup --mode hosted,deploy --shared,register, deploy-key deploys) was removed on 2026-10-24 and is restorable from commit5ef30882; the contract isdocs/hosted-vision.md.
The hosted Gateway (app.everyapp.com) is our own install of the same
Gateway a customer self-hosts, with the hosted policy
(GATEWAY_DEPLOYMENT_MODE=hosted, see
apps/every-app-gateway/src/server/platform-policy.ts and
docs/security-model.md, "Deployment Mode"):
- anyone signs up, into an organization of their own, and verifies their email first;
- first-party apps (OpenSEO) are shared: one Worker per app, owned by the platform organization, opened by members of any active organization, each reached over its service binding as on self-host;
- no Cloudflare Access sign-in (its settings are ignored);
- organization admins manage memberships, never accounts (no deactivating, resetting or deleting someone's account from an organization);
- every AI and capability call attributed to an organization and logged, with no per-organization usage limits yet (metering, the wallet, comes later);
- third-party deploys stay off (a launch gate, section 5).
Until Google verification and the wallet exist, the preview is invite-only:
set HOSTED_OPEN_SIGNUP=false (below) and people join by invitation.
1. Install
From a checkout of the repository, logged in to our Cloudflare account
(npx cf auth login, or CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID):
pnpm everyapp setup --domain app.everyapp.com --mode hosted # --mode removed (5ef30882)--mode was removed on 2026-10-24 (5ef30882, packages/cli/src/commands/setup/); the paragraph below describes it as it was.
This is the self-host install with one difference: the Gateway's first
upload sets GATEWAY_DEPLOYMENT_MODE=hosted, and the install record keeps
the mode (pnpm everyapp status shows · hosted after the Gateway's URL). The mode never
changes after the first upload; setup --mode self_hosted (removed) on this Gateway
stops with MODE_CHANGE_UNSUPPORTED. Once the first upload has succeeded,
later runs need no --mode, and a Gateway Worker deleted by hand comes
back, on the next setup or deploy, with the mode the install record keeps.
Until then, pass --mode hosted (removed) on every run: a run that resumes an install
stopped before its first upload, without --mode (removed), installs self_hosted
and keeps it.
Leave setup waiting at the owner step (below) or stop it and run it again
when you are ready: a new run shows a new owner link.
2. Secrets
Each is a Gateway setting (pnpm everyapp secret set <NAME>,
live as soon as it is set; the Gateway's code comes from pnpm everyapp setup). Apps never hold provider keys: the Gateway proxies them.
Email, needed for verification, invitations and password resets
(docs/runbooks/email-sending-setup.md). Without it the hosted Gateway
refuses every sign-up (EMAIL_NOT_CONFIGURED, logged as
auth.sign_up_refused): new accounts couldn't verify their email.
EMAIL_FROM, e.g.noreply@everyapp.com(the domain onboarded for Cloudflare Email Sending first), andEMAIL_FROM_NAME, e.g.EveryApp.EMAIL_REST_API_TOKEN, unless the Worker sends through itsEMAILbinding.
Mail is budgeted per UTC day and kind: 5 emails per address, and 2,000
verification, 1,000 reset and 2,000 invitation emails for the whole
Gateway. A refused mail logs email.send.failed with
EMAIL_BUDGET_EXHAUSTED.
Provider keys:
OPENAI_API_KEY: the AI proxy's OpenAI key.OPENROUTER_API_KEY: the AI proxy's OpenRouter key (OpenSEO's models).WORKERS_AI_API_TOKEN: the AI proxy's Workers AI token, an API token from the "Workers AI" template (Workers AI Read and Edit) on this account; inference runs and bills inCLOUDFLARE_ACCOUNT_ID, whichsetupalready set.DATAFORSEO_API_KEY: the integration proxy's DataForSEO credential.CONTEXT_API_KEY: the integration proxy's Context.dev key.
All five are settable (the integration proxy landed in #303).
Optional:
HOSTED_OPEN_SIGNUP:falsekeeps sign-up invite-only (the preview); unset ortruelets anyone sign up into an organization of their own. Any other value is invite-only.
Never set the CLOUDFLARE_ACCESS_* settings or EMAIL_PASSWORD_SIGN_IN on
the hosted Gateway: they are ignored there, and the Gateway logs that once
per isolate.
3. Claim the owner
setup opens the one-time owner link (at a terminal; under --json it is in
the claim_owner event). Create the owner account with it, under your own
name. The claim makes the install organization, which on the hosted Gateway
is the platform organization: it owns the first-party apps, and the claim
names it EveryApp (slug every-app) whatever the owner is called (a
self-hosted Gateway names it after the owner). Then confirm:
pnpm everyapp statusshows · hosted after the Gateway's URL and Owner <email> · EveryApp.
The owner's email counts as verified (the setup link is ours), so they sign in straight away. Open sign-up starts only after the claim.
4. Deploy the first-party apps
Each first-party app is a shared app: a registry row of the platform
organization with scope='shared', at <appId>.app.everyapp.com, reached
over its service binding (APP__<worker>) as on self-host, one Worker for
every organization. Any live member of any active organization opens it,
and its identity JWT carries that member's org_id and org_role: the app
scopes its data by them (OpenSEO scopes by organization). Only owners and
admins of the platform organization deploy
or register one: a Cloudflare login must be one of them by its verified
email (sign up on the Gateway with the same email); an API token
(CLOUDFLARE_API_TOKEN, CI) is the account's own authority. Both commands
refuse a Gateway whose install record isn't hosted
(SHARED_APPS_UNAVAILABLE).
An app the CLI builds, from its directory:
pnpm everyapp deploy --shared # --shared removed (5ef30882)--shared was removed on 2026-10-24 (5ef30882, packages/cli/src/commands/deploy/); the paragraph below describes it as it was.
It deploys as any app does, with two differences: the row is shared (nobody
is granted it, and it has no "Available to everyone" switch), and the Worker
gets no EVERY_APP_ORG_ID, since it serves every organization. An app keeps
the scope it was registered with: later deploys, and secret set|unset
(which redeploy it), keep it shared with or without --shared (removed), and still
need a platform owner or admin. An organization app's id can't become a
shared one (APP_SCOPE_MISMATCH; delete it and pick another id). Deleting a
shared app (everyapp delete) cuts every organization off from it, so it
too takes a platform owner or admin (SHARED_APP_FORBIDDEN otherwise).
A Worker deployed by other means (OpenSEO's Alchemy stage, say), in the Gateway's account:
pnpm everyapp register openseo --worker every-everyapp-openseo --manifest ../open-seo/everyapp.config.ts # register removed (5ef30882)everyapp register was removed on 2026-10-24 (5ef30882, packages/cli/src/commands/register/); the paragraph below describes it as it was.
register (removed) checks the Worker exists, validates the manifest as deploy
does, registers the app under the Worker name the Gateway gives it
(every-<platform org key>-<appId>, every-everyapp-<appId> for the
"EveryApp" organization; any other --worker stops with
WORKER_NAME_MISMATCH, which names it), records the Worker's live version,
redeploys the Gateway to bind it and publish its hostname, and waits for it
to go live. It never uploads or changes the Worker, and delete --purge
keeps it. Run it again after the Worker's own pipeline ships a version, so
the registry records it (the Gateway re-reads the app's tools on a new
version). A registered app's senders and inbound addresses are set
afterwards with pnpm everyapp email set --manifest ../open-seo/everyapp.config.ts …
(a hosted Gateway takes no app senders); register (removed) makes no routes.
--json prints the same events as deploy; the app.register event's
data.worker lists what the Worker's own pipeline must give it:
EVERYAPP_IDENTITY_ISSUER(identityIssuer) andEVERYAPP_IDENTITY_PUBLIC_KEYS(identityPublicKeys, JSON), so the SDK verifies the Gateway's identity JWTs;EVERY_APP_GATEWAY, a service binding toevery-app-gateway, entrypointAppGateway, with the props ingatewayBinding.props(organizationIdis the platform organization's): the AI proxy refuses any other;GATEWAY_URL, and noEVERY_APP_ORG_ID;- the plain-text vars in
vars:EVERYAPP_SHARED=1, so the SDK refuses a gateway call that says nobody's.
Provider calls from a shared app say whom they are for. Serving a request,
they forward its identity (for: request): the Gateway attributes the
call to that member's organization. Background work
(cron, Workflow) names the organization (for: { background: true, organizationId }, sent as x-everyapp-org), which the Gateway accepts
for an active organization with a live member and refuses with 403
otherwise; a call with neither is refused with 403.
5. Third-party deploys (the launch gate)
Customer code (forks of OpenSEO included) deploys through the Gateway's
deploy API into a Workers for Platforms dispatch namespace, never into the
account's own Workers (docs/security-model.md, "Hosted Deploy API").
It stays off until each of these is verified against a live namespace:
limits and the Outbound default-deny on every dispatch, the deployer's
binding allowlist and its read-back, identities minted for the Worker's
name, per-host sessions on the separate third-party domain, and the egress
default-deny. Off, the API's routes don't exist and no deploy key can be
made.
For the read-back, deploy an app with a D1 database and a Durable Object
class and check that the job succeeds: the comparison wants every field
the deployer sent back unchanged and lets Cloudflare add its own (a
service binding's environment, a Durable Object binding's
namespace_id and script_name), but a field the live API renames or
drops (a D1 binding's id, the gateway binding's props) fails every
deploy with BINDINGS_MISMATCH until bindingMismatches in
src/server/deploy/metadata.ts learns it.
A deploy runs in the request that starts it and the 30 seconds of
waitUntil after it: about 50 seconds in all. One that runs out (many
assets, slow migrations) stops where it was; if that is after the upload,
the Worker serves the new version while the registry row still records
the last one (a new app stays pending). Its job reads
DEPLOY_ABANDONED after 5 minutes, and the app can deploy again then. A
Durable Object or Queue runner is the fix.
What it needs, as Gateway settings (pnpm everyapp secret set <NAME>, from
any directory):
CF_DEPLOY_TOKEN: an API token for this account with Workers for Platforms (Workers Scripts) edit and D1 edit.DISPATCH_NAMESPACE: the namespace's name; the Gateway'sDISPATCHERbinding (added to the hosted install with the namespace, withoutbound = { service = "every-app-gateway", entrypoint = "Outbound", parameters = ["everyapp"] }) must name the same one.THIRD_PARTY_DOMAIN: the registrable domain apps are named on (<app>-<orgKey>.everyapp.dev), neverapp.everyapp.com: one that is, or is under or above,APP_BASE_DOMAINmakes every registration and deploy answerDEPLOYER_NOT_CONFIGURED. Unset, apps deploy but get no hostname. Any other value on the Gateway's own site (one whose last two labels areeveryapp.com) is treated as unset: the session cookie's SameSite protection depends on the two being different sites.
The deployer's account is the Gateway's own CLOUDFLARE_ACCOUNT_ID, which
the install writes (secret set refuses it): the namespace must be in the
Gateway's account.
Third-party apps open on their own hosts, each with its own session handed
over by the Gateway (docs/security-model.md, "Third-Party Hosts and
Per-Host Sessions"). The CLI publishes only the Gateway's own Custom
Domains, so the third-party domain is wired by hand, once:
- Add
everyapp.devas a zone in the Gateway's account. - A proxied wildcard DNS record:
AAAA *→100::(the Worker answers; the address is never reached). Universal SSL covers*.everyapp.dev. - A Workers route on that zone:
*.everyapp.dev/*→every-app-gateway. No route for the apex: it answers nothing.
After each everyapp setup or Gateway redeploy, check the route is still
listed on the Worker (Workers & Pages → every-app-gateway → Domains &
Routes): the CLI declares no route, and whether its deploy keeps one made
by hand is unverified against a live account.
The gate itself, THIRD_PARTY_DEPLOYS=on, is never set by the CLI: set it
by hand on the every-app-gateway Worker once the checks above pass, and
delete it to close the API again (deployed apps keep running; nobody can
deploy).
Then an organization's owner or admin makes a deploy key (Admin → Tokens → New token → Kind: Deploy key; 30 days by default, optionally bound to one app) and deploys from the app's directory, in CI too:
EVERYAPP_DEPLOY_TOKEN=<deploy key> EVERYAPP_GATEWAY_URL=https://app.everyapp.com pnpm everyapp deploy --json # removed (5ef30882)Deploy-key deploys from the CLI were removed on 2026-10-24 (deployToGateway.ts, hostedBundle.ts at 5ef30882); the /api/v1 API stays in the gateway.
Installing apps from GitHub
With the gate open, an organization's owner or admin can also install an
app without the CLI: Admin → Apps → Install app, paste
https://github.com/<owner>/<repo>, Install. The Gateway downloads the
repository's latest release asset everyapp-release.tar.gz (what
everyapp packs for a release: release.json, the tenant-neutral Build
Output and the migrations), checks its SHA-256 against release.json,
registers the app in the admin's organization and deploys it through the
same pipeline as the deploy API; the dialog follows the deploy job and
ends with the app's hostname. It is the same as a deploy key's deploy in
every rule above (policy, read-back, one deploy at a time, the 50-second
budget); deployed_by_actor is the admin's email and the job's source
column records the repository, tag, version and commit. A release's
manifest carries only the declarations (capabilities.email,
email: { inbound: true }); a third-party app can't declare inbound mail
(BUNDLE_REFUSED), and the hosted Gateway sends its mail from its own
sender.
NO_RELEASE: the repository's latest GitHub release has noeveryapp-release.tar.gz(or it has no releases, or it is private: the Gateway sends no credentials). Clone it and deploy with a deploy key as above.RELEASE_CORRUPT,RELEASE_INVALID,RELEASE_UNSUPPORTED: the asset isn't an app release this Gateway reads (a file doesn't match the SHA-256, a link or a path outside the archive, a Gateway release, a Build Output other thanv0).RELEASE_TOO_LARGE: over 100 MB compressed or unpacked.- The Gateway fetches from
github.comand GitHub's release storage (release-assets.githubusercontent.com/objects.githubusercontent.com) from its own Worker: nothing to configure.
With the gate shut the route answers APP_INSTALL_NOT_AVAILABLE. A
self-hosted Gateway has the same installer, on by default once
CF_DEPLOY_TOKEN is set: docs/runbooks/browser-app-install.md.
Checks
pnpm everyapp status --json:data.modeis"hosted".https://app.everyapp.com/api/auth/sign-in-methodsanswers"signUp": "open"("invitation"withHOSTED_OPEN_SIGNUP=false),"emailVerification": trueand no providers.A registry row with a
tierorscopethe Gateway doesn't know answers 503 on its host: fix the row, never the code.A shared app opens at
https://<appId>.app.everyapp.comfor a member of any organization;pnpm everyapp statuslists it.With third-party deploys wired: a third-party app at
https://<app>-<orgKey>.everyapp.devsends a signed-out browser throughhttps://app.everyapp.com/app-sessionand back, signed in, with a__Host-everyapp_scookie on the app host only; signing out of the Gateway signs the app host out on its next request. An unknown name undereveryapp.devanswers 404unknown_app_host.To suspend an organization, set its status in the Gateway's D1; within 30 seconds its members get 403 everywhere, and
'active'restores it:npx cf@1.0.0-beta.14 d1 list --name every-app-gateway # its id npx cf@1.0.0-beta.14 d1 query <id> \ --sql "UPDATE organizations SET status = 'suspended' WHERE slug = 'acme'"An owner link that answers
SETUP_UNAVAILABLEmeans the Worker'sGATEWAY_DEPLOYMENT_MODEsecret is missing or isn'tself_hostedorhosted(someone changed it by hand;everyapp secret setrefuses the name). Set it on theevery-app-gatewayWorker, in the Cloudflare dashboard, to the modepnpm everyapp statusprints.