Provision a product

A provisioning script that is safe to run on every deploy: find or onboard, the hook, facts, goals, scheduled spend, and what each answer looks like.

Embed
Preview of the card
<a href="https://seolyticize.com/docs/provision-a-product"><img src="https://seolyticize.com/docs/_api/public/orgs/71cb2b7a-a719-4003-b3ea-6321dc1db8ab/spaces/3bf2dbf7-3faa-473c-ba3b-30588f5f8de4/pages/76817c14-5c77-4d4d-ba54-8dbd6600898a/social-card.svg?v=3e7ae33eec8b" alt="Provision a product" width="600" height="315"></a>

For a team that onboards its own product from a script in its repo and runs that script again on every deploy. Every step below is safe to repeat.

Keys

Onboard with an agency key that can make clients (admin). Then give the product's agent its own key pinned to its client: create_api_key {name: "advernue-agent", clientId, role: "site_admin"}. A pinned key is refused every other client's data, so one product's agent cannot read another's.

The script, step by step

  1. Find or onboard. list_clients {domain: "advernue.example"} returns the client whose site is on that domain (www. and the scheme are ignored), with its domains. When the list is empty, onboard_site {domain, framework, verification, trackerFrequency: "weekly"}. Calling onboard_site again for the same domain finds the same client, so either way is safe.

  2. Deploy hook. create_deploy_hook {clientId, ifMissing: true} makes a hook only when there is none, and answers {created: false} without a URL otherwise. Without ifMissing, it rotates the URL. get_deploy_hook {clientId} answers exists.

  3. Brief. update_client_brief {clientId, add: [...]} adds an entry once: the same kind, title, text and address again returns the entry already there.

  4. Facts. Keep them in the repo as /.well-known/seolyticize.json ({"facts": [{"key": "starter plan price", "value": "$29/month", "category": "price"}]}) and call import_client_facts {clientId, dryRun: true}, then without dryRun. Every deploy hook call compares the published file with the registered facts and lists differences as factsDrift.

  5. Goals and funnels. create_goal {..., upsertBy: "name"} and create_funnel {..., upsertBy: "name"} return the existing one when it is defined the same way (created: false) and replace it when it is not. list_goals and list_funnels list them.

  6. Scheduled spend. get_scheduled_work {clientId} lists every paid job on a schedule with the call that pauses it. A new client has none: the domain sweep is off until set_domain_sweep turns it on, and the tracker bills only once it has keywords.

Dry runs

dryRun: true is taken by paid tools (it quotes and buys nothing) and by the writes that say so (set_client_facts, import_client_facts). Any other tool refuses it as an argument it does not take, rather than running for real.

Hosts without DNS

On a host you cannot add DNS records to (an sslip.io or preview host), use verification: "file" or "meta". verify_site then adds a URL-prefix Search Console property (https://host/), because a Domain property (sc-domain:) needs a TXT record. When the product gets its own domain, move_site keeps the client, its history and site key, and returns the new domain's manifest. Google's Change of Address has no API, so a person starts it in Search Console; until then the old property's history stays on the old property.

Signed-in apps

  • Analytics. The standard tag (a.js) sends the page's origin and path, never its fragment, and nothing of its query string but utm_* campaign tags, so ?ref= and tokens in the query never leave the page. A token in the path is the remaining risk: add excludePaths for the signed-in area (enforced on the server too), and pathTemplates for pages named after a customer.

  • Crawling. Keep the crawler out of the signed-in area with set_crawl_settings {clientId, excludePatterns: ["/dashboard/*", "/settings/*"]}: excluded pages are not fetched, so their redirects to the sign-in page are not reported. accept_issue marks a finding as intended. A new site onboarded with framework: "other" renders app shells before auditing them (render: "shells").

  • Conversions after the visit. A sign-up that finishes on another origin, or an invoice paid days later, is credited through the server collector: send the landing pageview from your server with the server key (issue_server_collect_key), keep the visit token it answers with on the account, and send the conversion later with visit. A token is never given to the browser, so nothing about a visit is stored on the visitor's device.

What the answers look like

onboard_site (shortened). files[].content is served exactly at files[].path:

{
  "clientId": "client_1a2b3c4d",
  "domain": "advernue.example",
  "created": true,
  "files": [{ "path": "/1f2e3d.txt", "content": "1f2e3d", "purpose": "IndexNow key: …" }],
  "head": [],
  "analytics": {
    "siteKey": "sk_example123",
    "snippet": "<script defer data-site=\"sk_example123\" src=\"https://seolyticize.com/a.js\"></script>",
    "install": "…",
    "suggestedExclude": "/api/*,/admin/*,/account/*,/dashboard/*,/login,/verify,/reset"
  },
  "csp": { "script-src": ["https://seolyticize.com"], "connect-src": ["https://seolyticize.com"] },
  "crawl": { "render": "shells", "why": "…" },
  "scheduledPaidWork": [],
  "next": "Deploy the files above … then call verify_site {clientId: \"client_1a2b3c4d\"}."
}

get_client: client (with domainSweep), projects[].sites, trackers (each with frequency) and analytics: {siteKey, collectUrl} at the top level. Nothing in it is secret.

Shown once, and only in the answer that makes them: the deploy hook url (create_deploy_hook), an API key, the server collect key, and a portal link.

Updated