Zero-friction flow runbook (EW-617)
Companion to the EW-617 spec series. Lives in the platform repo so it ships with the code; mirrored to
Workspace/knowledge/runbooks/EVER_WORKS_ZERO_FRICTION_FLOW.mdon the next Workspace sync.
What's wired up
| Gap | What it does | PR / status |
|---|---|---|
| G1 | Landing prompt + URL hand-off | ever-works-website#37 |
| G2 | Anonymous (TTL) users + cleanup task | platform #756 |
| G3 | Claim-account flow | platform #757 (stacked on G2) |
| G4 | Wizard "Generate now" + /api/works/quick-create | platform #758 |
| G5 | *.ever.works subdomain + Cloudflare DNS | platform #759 |
| G6 | Default deployProvider = 'ever-works' | platform #752 |
| G7 | Captcha + global caps | tracked, follow-up |
| G8 | Funnel telemetry + this runbook | platform (this PR) |
End-to-end demo
1. Open https://ever.works/ in a fresh-incognito window.
2. Type a directory description into the hero textarea.
3. Click Generate.
→ Browser redirects to https://app.ever.works/onboarding#prompt=…
4. Wizard mounts:
- Reads the prompt out of the URL fragment.
- Mints a temporary user via POST /api/auth/anonymous (G2).
- Jumps to the final "Generate now" step.
5. Click Generate now.
→ POST /api/works/quick-create (G4) creates the Work and starts
generation. Response: { work, generation.historyId }.
6. Background: the agent pushes 3 repos to ever-works-cloud (EW-614),
the deploy workflow runs (G5), Cloudflare CNAME is provisioned, and
the site comes up at https://<slug>.ever.works/.
7. Optional: post-deploy banner offers Claim Account
(POST /api/auth/claim, G3).
Debugging by stage
Each stage emits a [zero-friction] JSON log line via
ZeroFrictionFunnelService.emit (G8). All eight events share a
correlationId so a single correlationId=… filter follows one user
across services. Until the emit call sites land in their respective
PRs, fall back to the per-component logs below.
Stage 1 — landing prompt (G1)
- Browser console: form submit posts no XHR; it just sets
window.location.href. Open Network → Doc to see the redirect. - If the form doesn't appear: confirm the website build deployed
LandingPromptForm— check the rendered HTML in DevTools fordata-testid="landing-prompt-form".
Stage 2 — anon user (G2)
POST /api/auth/anonymousshould return 201 with{ access_token, user.isAnonymous: true }.- 429 → the per-IP throttle (5/hour) tripped. Log in or wait.
- 500 → check
AnonymousAuthServicelogs; common cause is theuserstable not yet havingis_anonymous/anonymous_expires_atcolumns. Run the G2 migration step or setdatabase.autoMigrate=truein dev.
Stage 3 — wizard finished (G4)
- The wizard reads the URL fragment on mount via the
EverWorksOnboardingWizardeffect. If the prompt doesn't pre-fill, the most likely cause is a hash-fragment URL encoding issue — open DevTools and inspectwindow.location.hashbefore any redirect.
Stage 4 — work created (G4)
POST /api/works/quick-createreturns 202 with{ status: 'pending', work, generation }. The work id from the response is what to use for downstream polling.- 400 → slug regex failed. The wizard derives slug from the prompt; if it produced an invalid slug log a bug.
Stage 5 — repos pushed (EW-614)
- Search
WorkLifecycleServicelogs forEverWorksGitProvider.createRepositorylines. Three repos should be created inever-works-cloud.
Stage 6 — deploy started (G5)
DeployService.deploylogs the workflow dispatch. TheK8S_INGRESS_HOSTAction variable should be set to${slug}.ever.works.- Cloudflare CNAME provisioning: search
CloudflareDns CNAME created/updatedinEverWorksDnsServicelogs.
Stage 7 — deploy ready
DeploymentVerifierServicepolls the cluster forReady. The site should respond onhttps://<slug>.ever.works/within ~2 min after the workflow finishes.
Stage 8 — claim account (G3)
- Authenticated
POST /api/auth/claimwith{ email, password }. 409 → email already taken by a different user. 403 → the caller's session is already non-anonymous.
Rolling back per gap
| Gap | Roll-back |
|---|---|
| G1 | Revert ever-works-website#37 and redeploy the website. The platform side keeps working — wizard hash-hydration is a no-op when no prompt is in the URL. |
| G2 | Revert platform #756. Existing anonymous rows orphan in the DB; safe to leave (the nightly cleanup task still runs once the schema columns exist). |
| G3 | Revert platform #757. Anonymous users stay anonymous; they can sign up normally via /register. |
| G4 | Revert platform #758. The wizard falls back to the legacy /works/new hand-off automatically (the "Generate now" button only renders when prompt is set). |
| G5 | Unset CLOUDFLARE_API_TOKEN (or rotate it). EverWorksDnsService.getProvider() returns null and the deploy pipeline reverts to the LB hostname fallback. |
| G6 | Revert platform #752. New rows go back to defaulting deployProvider='vercel'; existing rows are untouched. |
| G7 | (Not yet wired.) |
| G8 | Revert this PR. [zero-friction] log lines disappear; nothing else breaks. |
Configuration cheat-sheet
| Env var | Owned by | Notes |
|---|---|---|
ANONYMOUS_USER_TTL_DAYS | G2 | Default 7. Garbage values fall back to 7. |
CAPTCHA_PROVIDER | G7 | turnstile / hcaptcha / recaptcha or empty (disabled). |
CAPTCHA_SECRET | G7 | Server-side secret for the provider's /siteverify. |
CLOUDFLARE_API_TOKEN | G5 | Scoped: DNS:Edit on the ever.works zone only. Never the global key. |
CLOUDFLARE_ZONE_ID | G5 | 32-char hex. |
EVER_WORKS_DEPLOY_LB_HOSTNAME | G5 | Cluster ingress LB DNS name. Required for DNS automation. |
EVER_WORKS_DOMAIN | G5 | Defaults to ever.works. |
DEPLOY_EVER_WORKS_ENABLED | EW-608 | Gates the ever-works deploy provider end-to-end. |
NEXT_PUBLIC_APP_URL | G1 | Where the landing page sends users. Defaults to https://app.ever.works. |
Live values (dev / stage / prod, as of 2026-05-15)
| Env var | Value (or source) |
|---|---|
DEPLOY_EVER_WORKS_ENABLED | true (inlined in deploy-do-{dev,stage,prod}.yml) |
EVER_WORKS_DOMAIN | ever.works (inlined in workflows) |
EVER_WORKS_DEPLOY_LB_HOSTNAME | lb.ever.works → proxied CNAME → Cloudflare Tunnel (see "DNS anchor record") |
CLOUDFLARE_API_TOKEN | GH secret CLOUDFLARE_API_TOKEN (Zone:DNS edit on ever.works) |
CLOUDFLARE_ZONE_ID | GH secret CLOUDFLARE_ZONE_ID → 14b28d7fd73db5679d4280e4278ec6cf |
CAPTCHA_PROVIDER | GH secret CAPTCHA_PROVIDER — empty until Turnstile widget + client wiring ship |
CAPTCHA_SECRET | GH secret CAPTCHA_SECRET — empty until Turnstile widget + client wiring ship |
Deploy mechanism
Env vars land in pods via the envsubst step in
.github/workflows/deploy-do-{dev,stage,prod}.yml against the
.deploy/k8s/k8s-manifest.{dev,stage,prod}.yaml placeholders. Each
workflow's env: map maps GH secrets / inline values → $VAR →
container env. Adding a new env var = (a) declare - name: VAR / value: "$VAR" in the 3 manifest YAMLs, (b) pass VAR: ${{ secrets.VAR }} (or
inline) in the 3 workflow YAMLs, (c) gh secret set VAR --repo ever-works/ever-works.
DNS anchor record
lb.ever.works is the anchor that customer {slug}.ever.works CNAMEs
point at. Created via the Cloudflare API 2026-05-15; repointed
2026-07-29.
Current shape: lb.ever.works → proxied CNAME →
5a1c27a6-7d93-4f25-a329-3154995e16db.cfargotunnel.com, i.e. the same
Cloudflare Tunnel every other ever.works hostname uses. The tunnel has
a catch-all rule to ingress-nginx-controller.ingress-nginx on the
self-hosted ever-k8s cluster.