Skip to content

Documentation

Installing Heddle on a new site

This page takes your site from an empty folder to live with Heddle, in order. Your repository gets a heddle/ folder with your company's facts and settings, your build runs Heddle's generate, aeo and check steps, and the launch checklist goes to all PASS. heddle install does most of it in one command, and heddle launch checks every step outside the repository, from your Cloudflare zone to the agents' GitHub secrets.

Installing on a new property

From an empty folder (or an existing site) to built, checked and with the agents reporting, using only Heddle's commands. Heddle sits beside the site (../heddle) or HEDDLE_DIR points at it.

# 1. Once per machine, a person signs in to Cloudflare (a browser opens).
#    From Heddle's folder, so the pinned cf is the one used.
(cd ../heddle && npx cf auth login)

# 2. The company's facts. Either give the basics as flags and fill the rest
#    of heddle/*.json afterwards (heddle validate names every REPLACE: left),
#    or hand over a prepared heddle/ folder with --config.
node ../heddle/bin/heddle.mjs install my-site \
  --config path/to/prepared/heddle \
  --app next \
  --account <the Cloudflare account id that holds the zone> \
  --no-launch
#    or: --name "Brand" --url https://brand.example --contact hello@brand.example

# 3. Build and check.
cd my-site && git init && npm install
npm run build          # heddle generate, next build, heddle aeo; the first build records the search surface
npm run check          # validate, doctor, every gate

# 4. Push it to GitHub (the agents run there), then the outside steps.
gh repo create Owner/my-site --private --source . --push
npm run heddle:launch -- --apply   # KV (ids written into wrangler.jsonc), Email Routing, DMARC, tokens
npm run deploy                     # the first deploy (wrangler's own sign-in, or the deploy workflow)
npm run heddle:launch -- --apply   # again: the Worker exists now, so its shared secrets and mail rules can be set
npm run heddle:launch              # until nothing is TODO or FAIL; cut over last (uncomment the routes, deploy)

# 5. The agents' first reports.
node ../heddle/bin/heddle.mjs agent watchdog . --level report

heddle install writes.

heddle/ (from --config or templates/company), every managed template for the site's features (the Worker modules, the first-touch script, the build script, the agents and config-check workflows, and the ideas, approve and smoke workflows when the config calls for them), the Worker's entry and wrangler.jsonc once, package.json scripts and packages, the IndexNow key and its file, the pointer (or the skill) for coding agents in AGENTS.md and CLAUDE.md, and heddle/install.json, the record of what it installed.

--app next adds a starter Next.js site that reads every fact from heddle/. Then it runs heddle launch (skip with --no-launch; --apply passes through).

Later versions. heddle update <site> replaces each managed file with this Heddle's template and refuses one edited in the site (--force discards the edit).

heddle doctor <site> fails when a managed file differs from the template, when Heddle's machinery exists a second time in the site, or when config the installed features need is missing; npm run check and the config-check workflow run it, so drift fails the pull request that adds it.

Done when npm run build && npm run check pass, heddle launch shows nothing TODO or FAIL, and the watchdog's report is in heddle/reports/. npm run rehearse in Heddle runs steps 2, 3 and 5 for the fictional company in test/fixtures/new-property, plus a drift that doctor must catch.

What is still a person's, and why:

Step Why Heddle does not
npx cf auth login A browser sign-in, by design; everything after it is cf.
Adding the domain to Cloudflare and the registrar's nameservers Changes who answers for the domain; the registrar is outside Cloudflare.
GITHUB_DISPATCH_TOKEN (Worker secret) GitHub has no API that makes a fine-grained token. Make one for this repository only (Actions read and write) and npx wrangler secret put GITHUB_DISPATCH_TOKEN.
HEDDLE_REPO_TOKEN or HEDDLE_DEPLOY_KEY (GitHub secret) The same: read access to Heddle's private repository.
PLUNK_API_KEY, TURNSTILE_SECRET_KEY, the AI Gateway token, DataForSEO, the Search Console key Each comes from another account; heddle launch names where.
Clicking the destination address's verification email Cloudflare sends it to a person.
Search Console: adding the property and the service account Google has no API for proving ownership of a new domain property.
The first deploy and the cutover A deploy is a decision. Locally it needs wrangler's own sign-in (npx wrangler login); the deploy workflow uses the CLOUDFLARE_API_TOKEN that --apply stored.
The home page's copy The helpful and buyer gates judge what only the company can say. The starter passes every offline gate; the model gates fail it until the copy says something only this company knows.

Take it live

The order to take a new product site live with Heddle, and the command that checks every step. heddle launch <site-repo>. It is read-only. It prints each item as PASS, TODO (with the exact next action), FAIL (done wrong, or something in the way) or SKIP (could not be read, and why).

Run it at the start, after each step, and once more before cutover; the launch is done when nothing is TODO or FAIL.

heddle launch <site-repo> --apply does the safe steps the check found missing, and nothing else:

  • creates the KV namespaces wrangler.jsonc needs and writes their ids into it (a commented-out placeholder for the binding is replaced in place);
  • enables Email Routing on the zone (Cloudflare adds its own MX and SPF);
  • adds the destination address (Cloudflare emails it a link to click);
  • creates the hello@<domain> routing rule, and the ideas address's rule to the site's Worker when ideas by email is set up;
  • creates _dmarc with p=none;
  • adds the domain to the Turnstile widget's hostnames, keeping the others;
  • makes this property's own tokens and stores them as GitHub secrets. A deploy token (CLOUDFLARE_API_TOKEN: Workers Scripts and KV write and Account Settings read on the zone's account, Workers Routes write and Zone read on the zone) and, when site.json model.tokenEnv names one, a Workers AI Read token for the judging model;
  • sets CLOUDFLARE_ACCOUNT_ID as a GitHub secret (an identifier);
  • makes the secrets the Worker and the agents share (HEDDLE_LEADS_TOKEN, HEDDLE_IDEAS_TOKEN) at random and sets each on both sides at once, but only when both sides lack it. A value already set on one side cannot be read back, so that case is left for a person.

It only creates. It never edits or deletes a record it did not make, never overwrites a GitHub secret that is set, and refuses to enable Email Routing while another provider's MX records exist. That is a decision for a person. Records it creates carry the comment "created by heddle launch".

It lists every change before making it, then the result of each, and never prints a token or secret. Rerun without --apply to confirm.

Cloudflare through cf

cf is Cloudflare's own CLI (npm package cf, published by Cloudflare). Heddle pins it exactly (1.0.0-beta.14, in Heddle's package.json) and runs its own copy, never a global one. It is a beta.

A release can rename a command, so the pin moves only after npm test and a heddle launch against a real install pass on the new version.

  • A person signs in once with npx cf auth login; heddle launch checks the sign-in (cf auth whoami) and that it can see the account wrangler.jsonc names. The account is always passed explicitly (CLOUDFLARE_ACCOUNT_ID in cf's environment). A login that sees two accounts never picks one.
  • Each API call the launch makes is a cf command (lib/launch/cfcli.mjs ROUTES). Request bodies go on stdin, so no secret is ever in a process list, and cf's output is parsed, never printed.
  • cf has no command for a Worker's custom domains or the account's workers.dev subdomain. Those two reads fall back to the raw API when CLOUDFLARE_LAUNCH_TOKEN or CLOUDFLARE_API_TOKEN is set, and are SKIP otherwise.
  • CLOUDFLARE_LAUNCH_TOKEN, when set, still wins. The raw API with that token, as below. HEDDLE_CF=off turns cf off.

The launch token (without cf)

Only for a machine where cf cannot sign in. Create one token per Cloudflare account for launches, once:

  1. dash.cloudflare.com → the person icon, top right → My Profile → API Tokens → Create Token → Custom token: Get started.
  2. Name it Heddle launch (<account>).
  3. Add the eight permissions in the table below, one row each (+ Add more). The first dropdown is Account or Zone, the second the permission, the third Read or Edit.
  4. Account Resources: Include → the account the sites deploy to. Zone Resources: Include → All zones from an account → the same account, so the next domain needs no new token.
  5. Leave Client IP filtering empty. Set a TTL if you want it to expire.
  6. Continue to summary → Create Token, and copy it once. It is not shown again.
  7. Keep it in your password manager or a secrets file outside any repository (readable only by you, chmod 600), as CLOUDFLARE_LAUNCH_TOKEN=... on its own line. Check first that the file ends with a newline, or the new line glues onto the previous secret.
  8. Check it. curl -s -H "Authorization: Bearer $CLOUDFLARE_LAUNCH_TOKEN" https://api.cloudflare.com/client/v4/user/tokens/verify says "status":"active".

Keep it out of every repository and every GitHub secret. Launches run from a person's machine, never from CI.

Scope Permission Used for
Zone Zone: Read finding the zone and its account
Zone DNS: Edit reading records; creating _dmarc
Zone Email Routing Rules: Edit reading and creating the hello@ rule
Zone Zone Settings: Edit Email Routing status, and enabling it (the enable endpoint needs Edit)
Account Email Routing Addresses: Edit listing and adding destination addresses
Account Turnstile: Edit reading and updating the widget's hostnames
Account Workers KV Storage: Edit listing and creating KV namespaces
Account Workers Scripts: Read the Worker, its custom domains, secret names, workers.dev

Limit it to the zones it launches. Without it the check falls back to CLOUDFLARE_API_TOKEN and reports whatever that token cannot read as SKIP. --apply never borrows another token.

The steps

1. Domain. Register it, then add it to Cloudflare (Add a site) in the account the Worker deploys to. Custom domains attach only from the zone's account; the check fails a zone in a different account from wrangler.jsonc's account_id. Set the registrar's nameservers to Cloudflare's.

2. Email in: Cloudflare Email Routing, free. A product domain needs an address people can write to, and no mailbox of its own. Email Routing forwards hello@<domain> to an existing inbox, such as the team's Google or Microsoft mailbox. There is nothing to buy.

--apply enables it, adds the destination (click the link Cloudflare sends) and creates the rule. The destination is site.json launch.emailRoute.to, or the Worker's CONTACT_TO. If the domain already has MX records from Google or Microsoft, keep them and create the alias there instead; the check stops and leaves them in place.

Replying as hello@<domain> from Gmail is optional. Add it under Gmail → Settings → Accounts → "Send mail as", using the Plunk domain's SMTP details or another relay. Most teams reply from their own address, which is what the follow-up agent's drafts assume.

3. Email out: Plunk. The contact form, the agents' reports and approval links all send through Plunk. In Plunk → project settings → Domains → Add <domain>. It shows three DKIM CNAMEs, an SPF TXT and a bounce MX on the plunk. subdomain.

Add them in Cloudflare DNS, set to DNS only (grey cloud), then press Verify in Plunk.

Do not add Plunk's inbound MX. Plunk also offers an MX at the domain itself, inbound-smtp.<region>.amazonaws.com, for receiving mail into Plunk. Sending does not need it, and while it exists Cloudflare Email Routing will not enable. The Activate button shows "Existing non-Cloudflare MX records conflict with Email Routing" or appears to do nothing.

If it was added, delete that one record (the MX at the bare domain pointing to amazonaws.com), keep the plunk.<domain> MX, and enable Email Routing again. The check names this record when it finds it.

  • SPF: merge Plunk's include into the one SPF record. A domain may have only one; the check fails two.
  • The sender: once Plunk verifies the domain, set MAIL_FROM in wrangler.jsonc to noreply@<domain>. Until then it can stay on a domain Plunk already verified.
  • How the check reads Plunk:
    • from Plunk's API, with PLUNK_PROJECT_ID set beside PLUNK_API_KEY (a secret sk_ key);
    • otherwise from DNS, with the records copied into site.json launch.plunk.records.

3b. Your own mail filter. A brand-new sending domain is what phishing looks like, so the company's filter may quarantine its first mail. On one of our own sites, Microsoft Defender quarantined two short test emails sent straight to the CEO as "Phish", while the contact form's mail to a shared inbox got through.

Plunk still reports quarantined mail as Delivered (and the scanner's link check as Opened and Clicked), so look in the quarantine at security.microsoft.com → Email & collaboration → Review → Quarantine.

Select the domain's messages → More → Submit to Microsoft for review → "should not have been blocked" → Allow this message, with "45 days after last used date" (the agents' weekly mail keeps it alive), then Release.

Allow entries for a sending domain can only be made from a submission; the list itself takes no typed entry. Test with a real notification. A one-line "test" with a single link scores as phish on its own.

4. DMARC. --apply adds v=DMARC1; p=none at _dmarc, with rua=mailto: set to site.json launch.dmarcRua when given. Gmail and Yahoo require a DMARC record from bulk senders. Move to p=quarantine once reports show only Plunk and Cloudflare sending.

5. Turnstile, before cutover. The contact form's widget must list the domain, or every enquiry from it is refused from the minute the domain goes live. Dashboard → Turnstile → the widget → Hostnames → Add <domain> and www.<domain>. The check reads the site key from .env.local (NEXT_PUBLIC_TURNSTILE_SITE_KEY).

--apply adds the domain and keeps the existing hostnames (the workers.dev preview, other sites).

6. Deploy. In the site, for each missing KV binding (--apply creates them and prints the ids) and each missing secret: - npx wrangler secret put PLUNK_API_KEY - npx wrangler secret put TURNSTILE_SECRET_KEY - npx wrangler secret put HEDDLE_LEADS_TOKEN

Then npm run deploy (heddle deploy run . --local), which builds, runs every gate and deploys. That is the only deploy from a laptop. After step 8 the site deploys from CI (deploy.md). The Worker serves X-Robots-Tag: noindex on workers.dev, so the preview never competes with the domain; the check fails a preview without it.

7. Search Console. Add a Domain property for <domain> (verify with the TXT record in Cloudflare DNS). Then Settings → Users → add the service account as Restricted. The check reads the property list with the service-account key.

7b. Bing Webmaster Tools. Bing's index feeds Bing, Copilot and ChatGPT search, and Google's tools see none of it. 1. bing.com/webmasters → sign in → Import from Google Search Console. It verifies the domain and brings the sitemap with it.

Without a Search Console property, verify with the XML file or the DNS record instead. 2. Sitemaps → check https://<domain>/sitemap.xml is listed, or submit it. 3. IndexNow (step 9) reports into the same account under IndexNow.

  1. For the agents: Settings → API access → Generate API key, add "bing": true to site.json data.providers, and set the key as BING_WEBMASTER_API_KEY in the shell and as a GitHub secret.

It is done when Bing lists the sitemap as processed. From the first week, read Search Performance and AI Performance (citations in Copilot and Bing's AI answers) there each week. Do not measure Bing through DataForSEO.

Its Bing endpoint returned results for unrelated searches when tried on 10 September 2026, so Webmaster Tools is the only Bing reading worth having.

7c. Search data providers. Declare in site.json data.providers the providers the install will use (DataForSEO, Search Console and PageSpeed at least; Bing and OpenSEO when someone has made their keys). heddle launch lists them under "Search data" with each missing credential and what to create, and heddle data <site> status shows which capability each answers.

It is done when every provider declared reads PASS.

8. GitHub: the agents. - Copy templates/github/heddle-agents.yml to .github/workflows/. - Set the secrets the check lists, from wherever you keep them, with gh secret set NAME -R Owner/site. Never echo a value. - Give the workflow read access to Heddle with HEDDLE_REPO_TOKEN, a fine-grained token on the Heddle repository with Contents read-only.

Deploy keys are disabled in the org. - Enable the workflow and run the watchdog once by hand. gh workflow run heddle-agents.yml -f agent=watchdog. - Copy templates/github/heddle-deploy.yml too and set CLOUDFLARE_API_TOKEN (permissions in deploy.md).

From then on every push to the default branch, every merge an agent makes and a daily run deploy from CI, and each deploy is proven live before it counts as done.

Every agent starts at report; autonomy.json says which move up.

9. Cutover, last. Step 5 (Turnstile) must be done first. 1. Uncomment the <domain> and www.<domain> custom-domain routes in wrangler.jsonc and npm run deploy. Remove the HEDDLE_SITE_URL repository variable and point runtime.leads.url at the domain. 2. Run the check again: Custom domain, the live llms.txt, sitemap, robots and the contact endpoint should all PASS. 3.

Send one real enquiry through the form and see it arrive. 4. Submit the sitemap in Search Console. 5. IndexNow. heddle indexing key <site> once (commit site.json and the key file, deploy). The deploy workflow submits after every deploy and owns heddle/indexing.json; a laptop never submits (deploy.md).

Optional. Blog ideas by email. When autonomy.json has runtime.ideas, the check adds an "Ideas by email" section.

The routing rule for the ideas address must send to the site's Worker (not forward to a mailbox), the Worker must bind IDEAS and have HEDDLE_IDEAS_TOKEN and GITHUB_DISPATCH_TOKEN, each author's domain should publish DMARC, and the repository needs the heddle-ideas workflow and its secret. --apply never creates that rule.

Where mail goes is a person's decision. The whole setup is in ideas-by-email.md.

Settings it reads

Everything has a default; site.json launch overrides it.

Field Default
worker wrangler.jsonc
repo the git origin remote
contactPath /api/contact
emailRoute.local, emailRoute.to hello; wrangler.jsonc vars.CONTACT_TO
dmarcRua none
turnstileSitekeyEnv NEXT_PUBLIC_TURNSTILE_SITE_KEY
workerSecrets PLUNK_API_KEY, TURNSTILE_SECRET_KEY, HEDDLE_LEADS_TOKEN
kvBindings RATE_LIMIT, LEADS
plunk.projectIdEnv, plunk.records PLUNK_PROJECT_ID; none

--preview <url> (or HEDDLE_SITE_URL) also checks the preview host's llms.txt, sitemap, robots and contact endpoint before the domain is attached.

Generated from the Heddle repository at ddb1b16.

Questions

How do I know the install is finished?

When heddle validate reports the folder valid, heddle check passes on the built site, and heddle launch shows nothing as TODO or FAIL.

Can the launch step change my DNS?

Only with --apply, and only by creating: KV namespaces, Email Routing, the hello@ rule, a DMARC record at p=none and Turnstile hostnames. It never edits or deletes a record it did not make, and it lists every change before making it.