# Installing Porchsite (for the assistant)

You are an AI assistant installing Porchsite 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.porchsite.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.

**Moving the files.** `sitekeeper.js` is about 1.6 MB, and step 6 sends it to
Cloudflare. Fetch it with your own web or shell tool, then send it with an
HTTP tool that can carry a file or a long body. Cloudflare's MCP `execute`
sandbox can't fetch outside addresses itself, so if that's your route you
must pass the file's contents into the call (some apps let you reference a
local file in a tool's arguments). If you can do neither, ask your person
for an API token (step 0) and use your own HTTP tool for steps 3 and 6.

List calls (`GET` of databases, namespaces, zones) return a page at a time:
add `?per_page=50` and check `result_info` for more.

## 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 `porchsite` 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

Porchsite 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"}`; the names are at
`result[0].results[].name`. 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.
A file may hold several statements; one call runs them all.

## 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, and delete any file
you built for step 6's upload too: it contains the code.

## 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.

If your tool can't build `multipart/form-data` itself, write the body as
text: pick a boundary (e.g. `----porchsite`), then for each part a line
`------porchsite`, its headers (`Content-Disposition: form-data;
name="metadata"` or `name="sitekeeper.js"; filename="sitekeeper.js"`, and
`Content-Type`), a blank line and the content; end with `------porchsite--`.
Send it raw (not JSON-encoded) with the header `Content-Type:
multipart/form-data; boundary=----porchsite`.

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://porchsite.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, "previews_enabled": false}` (preview addresses for
   each upload are on by default; the site doesn't need them).

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. `www.<domain>` can take a few minutes longer
than the bare domain to get its certificate; that's normal.

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.

Then remove the code from the site: `DELETE
/accounts/ACCOUNT/workers/scripts/NAME/secrets/SETUP_CODE`. The site no
longer needs it, and it stays stored until you delete it (uploading again
without it doesn't remove it).

## 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. If a `SETUP_CODE` secret is still listed
   (`GET /accounts/ACCOUNT/workers/scripts/NAME/secrets`), delete it as in
   step 8.
3. Check what's running: every page answers with an `X-Porchsite-Release`
   header naming the version, and `site_state` returns it as `release`.
4. 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.
