# Installing Sitekeeper (for the assistant)

You are an AI assistant installing Sitekeeper on your person's own Cloudflare
account. Follow these steps in order. Every step is one Cloudflare API call;
nothing needs git, a terminal or a build tool. When a step says "tell your
person", say it in plain words and wait for them.

Get the files from `https://get.sitekeeper.dev/latest.json`. It lists
the current `version` and its `files`, each with a `url` and a `sha256`:
`sitekeeper.js` (the site) and `migrations/*.sql` (the database layout).
Download each one and check its sha256 before you use it. Tell your person
which version you installed.

## 0. Access

Calls go to `https://api.cloudflare.com/client/v4` with your Cloudflare
access: either Cloudflare's own MCP server (its `execute` tool), or an API
token your person made. If it's a token, collect it through your app's
secret or password field, never in the chat, and never write it into a
memory, a note or a file. The access must be able to **edit** Workers scripts,
D1 and Workers KV. If you connected through Cloudflare's MCP server, your
person had to choose write access on its consent screen; read-only is the
default and will fail at step 2.

Find the account: `GET /accounts`. If there is more than one, ask your person
which. Call its id `ACCOUNT` below.

## 1. Pick a name

Use `sitekeeper` unless your person wants another. It becomes part of the
address: `https://<name>.<subdomain>.workers.dev`. Call it `NAME`.

## 2. Database

`POST /accounts/ACCOUNT/d1/database` with `{"name": "NAME"}`.
Keep `result.uuid` as `DB`.

Already exists (error code 7502)? `GET /accounts/ACCOUNT/d1/database?name=NAME`
and use that one's uuid. Never delete a database to start over: it holds the
site.

## 3. Database layout

Sitekeeper records which layout files it has applied, so this step is safe to
repeat. First:

`POST /accounts/ACCOUNT/d1/database/DB/query` with
`{"sql": "CREATE TABLE IF NOT EXISTS _sitekeeper_migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)"}`

Then `{"sql": "SELECT name FROM _sitekeeper_migrations"}`. For each file in
`migrations/`, in name order, that is not in that list: send the file's whole
text as `sql` to the same endpoint, and when it succeeds, record it:
`{"sql": "INSERT INTO _sitekeeper_migrations VALUES (?, datetime('now'))", "params": ["0001_init.sql"]}`.
If a file fails, stop and show your person the error; do not record it.

## 4. Sign-in storage

`POST /accounts/ACCOUNT/storage/kv/namespaces` with `{"title": "NAME-oauth"}`.
Keep `result.id` as `KV`. Already exists? List them
(`GET /accounts/ACCOUNT/storage/kv/namespaces`) and use the one with that title.

## 5. Setup code

Make a random code of at least 16 letters and digits. Call it `CODE`. Your
person types it once, in step 8, to claim the site. Don't put it anywhere but
step 6 and your message in step 8. If you must keep it in a file meanwhile,
delete the file once your person has claimed the site.

## 6. Upload the site

`PUT /accounts/ACCOUNT/workers/scripts/NAME` as `multipart/form-data` with
two parts:

- `metadata` (`application/json`):
  ```json
  {
    "main_module": "sitekeeper.js",
    "compatibility_date": "2026-08-15",
    "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
    "bindings": [
      {"type": "d1", "name": "DB", "id": "DB"},
      {"type": "kv_namespace", "name": "OAUTH_KV", "namespace_id": "KV"},
      {"type": "secret_text", "name": "SETUP_CODE", "text": "CODE"}
    ],
    "observability": {"enabled": true}
  }
  ```
  with your `DB`, `KV` and `CODE` values in place of the quoted names.
- `sitekeeper.js` (`application/javascript+module`): the file's contents,
  with that part name and filename.

Updating later is the same call; see "Updating" at the end.

## 7. Turn on the address

Every account has one workers.dev name, shared by all its Workers: the site's
address is `https://NAME.<that name>.workers.dev`. Most people use their own
name or nickname, e.g. `sam` gives `https://sitekeeper.sam.workers.dev`.

1. `GET /accounts/ACCOUNT/workers/subdomain`. If it returns a `subdomain`,
   keep it. On a new account it fails with HTTP 404, error code 10007 ("You
   do not have a workers.dev subdomain"). That is expected, not a problem,
   and you don't need the dashboard it mentions: ask your person to pick a
   short name, then `PUT /accounts/ACCOUNT/workers/subdomain` with
   `{"subdomain": "<their pick>"}`. Taken? Ask for another.
2. `POST /accounts/ACCOUNT/workers/scripts/NAME/subdomain` with
   `{"enabled": true}`.

The site is `https://NAME.<subdomain>.workers.dev`. A brand-new address
takes a minute or two to answer: request `https://…/setup` every 20 seconds,
treat connection or TLS errors as "not ready yet", and go on once it answers
200. Don't send your person there before that.

## 7b. A free address, or their own domain

Ask your person now, before step 8, in plain words:

> "Your site can live at the free address `https://NAME.<subdomain>.workers.dev`,
> or at a domain you own, like `yourname.com`. A domain costs about $10 to $15
> a year. Which would you like? You can add a domain later, but it's simpler now."

Settle it here. A passkey works only at the address it was made on, so your
person must claim the site at its final address in step 8.

**Free address:** nothing to do; go to step 8.

**Their own domain:** find out which case it is, then follow that path.

**A. The domain is already on this Cloudflare account.**
`GET /zones?name=<domain>` returns it. Keep `result[0].id` as `ZONE` and
attach it (below).

**B. They own the domain, but it's registered somewhere else** (GoDaddy,
Namecheap, Squarespace, …).
1. Add it to Cloudflare: `POST /zones` with
   `{"name": "<domain>", "account": {"id": "ACCOUNT"}, "type": "full"}`. Keep
   `result.id` as `ZONE` and `result.name_servers` (two names).
2. Tell your person: "Sign in where you bought the domain, find its
   nameservers, and replace them with these two: …". Offer to walk them
   through it step by step for their registrar. If the domain already runs
   email or another site, warn them first: Cloudflare copies the existing DNS
   records it finds, but they should check those records before switching.
3. Wait. `GET /zones/ZONE` until `status` is `active`. It usually takes
   minutes, but can take up to a day. `PUT /zones/ZONE/activation_check` asks
   Cloudflare to look again (once an hour at most). Tell your person how long
   it's been, and stay with step 7b until it's active.

**C. They don't have a domain yet.** Cloudflare sells domains at cost. Tell
your person: "In the Cloudflare dashboard, open Domain Registration, then
Register Domains, search for the name you want, and buy it. I can't buy it
for you." Once it shows in `GET /zones?name=<domain>`, carry on as in case A.

**Attach it.** The site lives at the bare domain (`sam.com`, not
`www.sam.com`). `PUT /accounts/ACCOUNT/workers/domains` with
`{"hostname": "<domain>", "service": "NAME", "zone_id": "ZONE"}`, then the
same call with `"hostname": "www.<domain>"`, so people who type www still
arrive: the site sends them on to the bare domain. Cloudflare
creates the DNS record and the certificate itself. If the hostname already
has a DNS record (an old site, a parking page), the call fails. Show your
person the error, and don't delete their record without asking.

A new domain takes a few minutes to answer. Request `https://<domain>/setup`
every 30 seconds, treat connection or TLS errors as "not ready yet", and go on
once it answers 200. From here on, the site's address is `https://<domain>`;
use it in every step below.

Access for this step: adding and attaching a domain needs **Zone: Edit**,
**DNS: Edit** and **Workers Routes: Edit** on top of step 0's access. If a
call is refused for permissions, tell your person which one to add to the
token (or to choose on Cloudflare's consent screen), and wait.

## 8. Your person claims it

Tell your person: "Your site is ready. Open `https://…/setup`, enter the code
CODE, and save a passkey. That passkey is how you approve what I write."
Wait until they say it's done.

## 9. Connect yourself

Tell your person to add a connector in their assistant settings with the
address `https://…/mcp`, then approve it on the page that opens. Their
assistant app may ask them to confirm twice: once in the app, once on the
site's own page with their passkey. Both are expected. After that,
use the tools it gives you; start with the one that describes the site.

## Check

- `GET https://…/` answers with the site.
- `GET https://…/setup` after step 8 sends you to `/login`: the site is claimed.
- Nothing you write shows on the site until your person approves it at
  `https://…/review`. That is on purpose.

## Images

Image fields (the site's `hero`, an entry's `cover`, a card's `image`) and
Markdown images in an entry's body take a full https:// link to a JPEG, PNG
or WebP, up to 8 MB. The site copies the image in when you save, and the item
then holds a `/media/...` path instead; reuse that path freely. Every image
needs a few words saying what it shows (`hero_alt`, `cover_alt`, `image_alt`,
or the text in `![...]`).

A photo your person sent you: call `photo_upload_link`, then POST the file to
the `url` it returns as `multipart/form-data`, with fields `photo` (the file)
and `alt` (what it shows), and header `Accept: application/json`. The answer
holds `image`, the path to use. The link works once, for 30 minutes. If you
can't send files, give the link to your person instead: the page lets them
pick the photo on their phone (and shrinks it first), and
`photo_upload_status` tells you when it's done.

## Moving to a new address later

If the site was claimed at one address and later gets a domain, your person's
passkey won't work at the new one. Attach the domain as in step 7b, then tell
your person: "Sign in at the old address, open Passkeys, and make a link.
Open it at the new address and save a passkey there." Then they reconnect you
at `https://<domain>/mcp`.

## Updating

When `latest.json` names a newer version than the one you installed, tell
your person what changed and ask before updating. Then:

1. Step 3 again: it applies only the layout files not yet recorded.
2. Step 6 again, with the same `DB` and `KV`, and without the `SETUP_CODE`
   binding (the site is already claimed). The database and everything in it
   stay as they are.
3. If the update adds tools, your person may need to remove the site's
   connector in their assistant app and add it again before you see them.
