Website delivery and file processing are different
Entry point and migration boundary
- Current site:securetools.app
- currently serves Web Utilities. A later ecosystem migration may move this product,
- but no future subdomain is linked or implied here.
+ Web application:tools.securetools.app
+ hosts the parallel Web Utilities deployment and becomes the product's canonical
+ public endpoint during the coordinated H3.5 cutover.
diff --git a/public/project/index.html b/public/project/index.html
index e597cb3..588c882 100644
--- a/public/project/index.html
+++ b/public/project/index.html
@@ -4,14 +4,22 @@
+
+
+
+
+
+
+
+
Project — Secure Tools
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..550290e
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://securetools.app/sitemap.xml
diff --git a/public/sitemap.xml b/public/sitemap.xml
new file mode 100644
index 0000000..bdbb079
--- /dev/null
+++ b/public/sitemap.xml
@@ -0,0 +1,13 @@
+
+
+ https://securetools.app/
+ https://securetools.app/products/
+ https://securetools.app/products/web-utilities/
+ https://securetools.app/products/desktop-pet/
+ https://securetools.app/products/local-ai/
+ https://securetools.app/libraries/
+ https://securetools.app/libraries/secure-metadata/
+ https://securetools.app/principles/
+ https://securetools.app/principles/privacy/
+ https://securetools.app/project/
+
diff --git a/scripts/validate-h3-hub-cutover.py b/scripts/validate-h3-hub-cutover.py
new file mode 100644
index 0000000..f3507e2
--- /dev/null
+++ b/scripts/validate-h3-hub-cutover.py
@@ -0,0 +1,155 @@
+#!/usr/bin/env python3
+"""Validate the prepared H3 Hub SEO and crawler contract."""
+
+from __future__ import annotations
+
+import re
+import struct
+import sys
+import xml.etree.ElementTree as ET
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[1]
+PUBLIC = ROOT / "public"
+ORIGIN = "https://securetools.app"
+SOCIAL_IMAGE = f"{ORIGIN}/assets/brand/social-preview.png"
+EXPECTED_ROUTES = {
+ "/",
+ "/products/",
+ "/products/web-utilities/",
+ "/products/desktop-pet/",
+ "/products/local-ai/",
+ "/libraries/",
+ "/libraries/secure-metadata/",
+ "/principles/",
+ "/principles/privacy/",
+ "/project/",
+}
+EXPECTED_HEADERS = """https://secure-tools-hub-53i.pages.dev/*
+ X-Robots-Tag: noindex, nofollow
+
+https://:version.secure-tools-hub-53i.pages.dev/*
+ X-Robots-Tag: noindex, nofollow
+"""
+
+
+def route_for(index_file: Path) -> str:
+ relative = index_file.relative_to(PUBLIC).as_posix()
+ return "/" if relative == "index.html" else f"/{relative.removesuffix('index.html')}"
+
+
+def values(markup: str, pattern: str) -> list[str]:
+ return re.findall(pattern, markup, flags=re.IGNORECASE)
+
+
+def main() -> int:
+ errors: list[str] = []
+ page_titles: set[str] = set()
+ page_descriptions: set[str] = set()
+ pages = {route_for(path): path for path in PUBLIC.rglob("index.html")}
+ if set(pages) != EXPECTED_ROUTES:
+ errors.append(
+ f"Hub route inventory mismatch: missing={sorted(EXPECTED_ROUTES - set(pages))}, "
+ f"unexpected={sorted(set(pages) - EXPECTED_ROUTES)}"
+ )
+
+ for route, path in sorted(pages.items()):
+ markup = path.read_text(encoding="utf-8")
+ expected_url = f"{ORIGIN}{route}"
+ checks = {
+ "canonical": values(markup, r'([^<]+)")
+ description = values(markup, r'II", image_bytes[16:24]) != (1200, 630):
+ errors.append("social preview dimensions must be exactly 1200 x 630")
+ except OSError as error:
+ errors.append(f"social preview is unavailable: {error}")
+
+ if errors:
+ for error in errors:
+ print(f"ERROR: {error}", file=sys.stderr)
+ return 1
+
+ print(f"Validated {len(pages)} Hub routes and canonical metadata records.")
+ print(f"Validated {len(locations)} unique Hub sitemap URLs and robots.txt.")
+ print("Validated Pages-alias noindex configuration and 1200 x 630 PNG social image.")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/validate-h3-url-map.py b/scripts/validate-h3-url-map.py
index fad082a..324bddd 100644
--- a/scripts/validate-h3-url-map.py
+++ b/scripts/validate-h3-url-map.py
@@ -12,6 +12,7 @@
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
MAP_PATH = REPOSITORY_ROOT / "docs" / "migrations" / "h3-url-map.csv"
PUBLIC_ROOT = REPOSITORY_ROOT / "public"
+REDIRECTS_PATH = PUBLIC_ROOT / "_redirects"
EXPECTED_FIELDS = ["old_url", "new_url", "status", "reason"]
@@ -35,6 +36,7 @@ def main() -> int:
redirect_count = 0
root_count = 0
current_hub_routes = hub_routes()
+ expected_redirects: dict[str, str] = {}
for line_number, row in enumerate(rows, start=2):
old_url = row.get("old_url", "")
@@ -73,9 +75,57 @@ def main() -> int:
errors.append(f"line {line_number}: redirect target does not preserve {old.path}")
if old.path in current_hub_routes:
errors.append(f"line {line_number}: legacy redirect collides with Hub route {old.path}")
+ expected_redirects[old.path] = new_url
if root_count != 1:
errors.append(f"expected one apex root reservation; found {root_count}")
+ if redirect_count != 18:
+ errors.append(f"expected 18 redirect inventory rows; found {redirect_count}")
+
+ actual_redirects: dict[str, str] = {}
+ if not REDIRECTS_PATH.is_file():
+ errors.append("public/_redirects is missing")
+ else:
+ for line_number, raw_line in enumerate(
+ REDIRECTS_PATH.read_text(encoding="utf-8").splitlines(), start=1
+ ):
+ line = raw_line.strip()
+ if not line or line.startswith("#"):
+ continue
+ fields = line.split()
+ if len(fields) != 3:
+ errors.append(f"_redirects line {line_number}: expected source destination status")
+ continue
+ source, destination, redirect_status = fields
+ target = urlsplit(destination)
+ if source in actual_redirects:
+ errors.append(f"_redirects line {line_number}: duplicate source {source}")
+ if "*" in source or ":" in source:
+ errors.append(f"_redirects line {line_number}: wildcard or placeholder is not allowed")
+ if source == "/":
+ errors.append("_redirects must not redirect the Hub root")
+ if source in current_hub_routes:
+ errors.append(f"_redirects line {line_number}: Hub route collision {source}")
+ if redirect_status != "301":
+ errors.append(f"_redirects line {line_number}: status must be 301")
+ if target.scheme != "https" or target.netloc != "tools.securetools.app":
+ errors.append(f"_redirects line {line_number}: target host must be tools.securetools.app")
+ if source != target.path:
+ errors.append(f"_redirects line {line_number}: target must preserve path {source}")
+ actual_redirects[source] = destination
+
+ if actual_redirects != expected_redirects:
+ missing = sorted(expected_redirects.keys() - actual_redirects.keys())
+ unexpected = sorted(actual_redirects.keys() - expected_redirects.keys())
+ mismatched = sorted(
+ source
+ for source in expected_redirects.keys() & actual_redirects.keys()
+ if expected_redirects[source] != actual_redirects[source]
+ )
+ errors.append(
+ "public/_redirects does not exactly match h3-url-map.csv "
+ f"(missing={missing}, unexpected={unexpected}, mismatched={mismatched})"
+ )
if errors:
for error in errors:
@@ -86,6 +136,7 @@ def main() -> int:
f"Validated {len(rows)} inventory rows: "
f"{redirect_count} redirects and {root_count} Hub-root no-redirect reservation."
)
+ print(f"Validated {len(actual_redirects)} exact Cloudflare Pages redirect rules.")
print(f"Checked collisions against {len(current_hub_routes)} current Hub routes.")
return 0
From 9f1d88ec872b0bb8a1a194f9c1aa93b495fc9f9c Mon Sep 17 00:00:00 2001
From: maruson08
Date: Tue, 1 Sep 2026 16:10:26 +0900
Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D[Docs]=20Define=20coordinated?=
=?UTF-8?q?=20H3.5=20cutover=20runbook?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 2 +
docs/architecture.md | 10 +-
docs/deployment.md | 23 +++-
docs/migrations/h3-migration-contract.md | 12 ++
docs/migrations/h3.5-cutover-runbook.md | 165 +++++++++++++++++++++++
docs/seo.md | 55 ++++++++
6 files changed, 259 insertions(+), 8 deletions(-)
create mode 100644 docs/migrations/h3.5-cutover-runbook.md
create mode 100644 docs/seo.md
diff --git a/README.md b/README.md
index 66698cb..ac1db18 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,8 @@ validation target.
- [Architecture](docs/architecture.md)
- [Privacy model](docs/privacy-model.md)
- [Deployment transparency](docs/deployment.md)
+- [Search metadata](docs/seo.md)
+- [H3.5 cutover runbook](docs/migrations/h3.5-cutover-runbook.md)
- [H2.2 quality assurance](docs/h2.2-qa.md)
The static site can be previewed by serving `public/` with any local static file server.
diff --git a/docs/architecture.md b/docs/architecture.md
index 043f135..114a216 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -44,11 +44,11 @@ of the circular CSS brand mark used by Web Utilities. The 1200×630 social previ
specifically for the Hub and uses its catalog language and visual system; it is not a copy of
the Web Utilities social image.
-The social preview asset is ready, but host-dependent `og:image` and `twitter:image` metadata
-is intentionally deferred until a separately reviewed production-domain migration establishes
-the permanent Hub origin. Canonical and `og:url` metadata is deferred for the same reason. This
-avoids coupling the public identity to a temporary validation hostname or claiming
-`securetools.app` before migration.
+H3.4B prepares the reviewed social preview at its final absolute
+`https://securetools.app/assets/brand/social-preview.png` URL together with self-referencing
+Hub canonical and `og:url` metadata. These source changes remain inactive while the H3.4B
+pull request is unmerged. Pages validation aliases are covered by hostname-specific noindex
+headers so they do not become alternate public identities after coordinated H3.5 activation.
## Product disclosure contract
diff --git a/docs/deployment.md b/docs/deployment.md
index bc5263c..935012d 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -41,9 +41,9 @@ permissions.
## Current validation deployment
-The current deployment target is the Cloudflare Pages-managed `secure-tools-hub.pages.dev`
-site. This endpoint is for validating the Hub deployment path and does not mean that the Hub
-has become the Secure Tools production site.
+The current deployment target is the Cloudflare Pages-managed
+`https://secure-tools-hub-53i.pages.dev` site. This endpoint is for validating the Hub
+deployment path and does not mean that the Hub has become the Secure Tools production site.
## Future production domain
@@ -53,3 +53,20 @@ perform the future production migration.
Any production domain migration and related redirects must be handled as a separate,
explicitly reviewed milestone.
+
+## Prepared H3.5 deployment contract
+
+The unmerged H3.4B branch prepares static `public/_headers` and `public/_redirects`
+artifacts. The hostname-specific header rules keep the stable and immutable
+`*.secure-tools-hub-53i.pages.dev` aliases non-indexable without applying noindex to the
+future custom domain. The redirects contain only the 18 explicit H3.1 legacy Web Utilities
+paths and preserve each path on `https://tools.securetools.app`; the Hub root and all Hub
+routes are excluded.
+
+Cloudflare Pages path redirects do not implement the future `www → apex` domain redirect.
+That change requires a Cloudflare zone/account-level Redirect Rule or Bulk Redirect plus
+proxied DNS during the coordinated H3.5 window. The exact activation and rollback order is
+documented in [the H3.5 cutover runbook](./migrations/h3.5-cutover-runbook.md).
+
+The H3.4B pull request does not attach a custom domain, change DNS, activate a `www` rule,
+modify Search Console, or deploy from its feature branch.
diff --git a/docs/migrations/h3-migration-contract.md b/docs/migrations/h3-migration-contract.md
index af59426..fd2c92d 100644
--- a/docs/migrations/h3-migration-contract.md
+++ b/docs/migrations/h3-migration-contract.md
@@ -158,3 +158,15 @@ Every gate requires recorded evidence and an explicit go/no-go decision. A failu
5. **H3.6 — Search Migration Monitoring:** submit host-specific sitemaps and monitor indexing, redirects, availability, and rollback signals.
No later phase is authorized by this document alone.
+
+## H3.4B preparation
+
+[Issue #14](https://github.com/SecureToolsProject/hub/issues/14) prepares the Hub-owned H3.5
+artifacts on an unmerged branch: final Hub metadata, sitemap and robots files,
+hostname-specific Pages-alias noindex headers, and the 18 explicit redirects derived from this
+inventory. The executable sequence and rollback requirements are in
+[h3.5-cutover-runbook.md](./h3.5-cutover-runbook.md).
+
+These files do not authorize or perform a merge, deployment, apex or `www` DNS change,
+custom-domain attachment, zone redirect, Search Console operation, or Secure_Tools PR #73
+merge. H3.5 must coordinate those operations only after all preflight gates pass.
diff --git a/docs/migrations/h3.5-cutover-runbook.md b/docs/migrations/h3.5-cutover-runbook.md
new file mode 100644
index 0000000..d538334
--- /dev/null
+++ b/docs/migrations/h3.5-cutover-runbook.md
@@ -0,0 +1,165 @@
+# H3.5 coordinated cutover runbook
+
+Status: prepared by H3.4B; do not execute from the preparation pull request.
+
+This runbook coordinates the final architecture:
+
+```text
+https://securetools.app
+→ Secure Tools Project Hub
+
+https://tools.securetools.app
+→ Secure Tools Web Utilities
+```
+
+The reviewed Hub preparation includes canonical and social metadata, a Hub-only sitemap,
+production robots policy, Pages-alias noindex headers, and 18 explicit legacy redirects.
+Secure_Tools PR #73 owns the separately reviewed Web Utilities SEO preparation. Neither
+preparation pull request may be merged outside the H3.5 change window.
+
+## Preconditions and ownership
+
+- A named operator owns the Cloudflare, GitHub, DNS, TLS, HTTP, and rollback decisions.
+- A second reviewer confirms recorded snapshots and the go/no-go decision.
+- No Search Console change occurs until the HTTP, TLS, canonical, and redirect gates pass.
+- Do not use Search Console Change of Address: this is a partial migration and the apex
+ remains active as the Hub.
+- Preserve the existing Secure_Tools GitHub Pages workflow, repository `CNAME`, and custom
+ domain during initial stabilization unless a separately reviewed change requires removal.
+
+## A. Preflight and snapshots
+
+1. Confirm Hub H3.4B and Secure_Tools PR #73 are clean, reviewed, current with each repository's
+ `main`, and unmerged.
+2. Record the exact head SHA of both preparation PRs and the current `main` SHA of both
+ repositories.
+3. Verify all Hub repository validation and all Secure_Tools validation.
+4. Verify HTTPS and expected content on:
+ - `https://secure-tools-hub-53i.pages.dev`;
+ - `https://tools.securetools.app`;
+ - `https://secure-tools-web-bridge.pages.dev`; and
+ - the existing `https://securetools.app` GitHub Pages production.
+5. Snapshot, without exposing credential values:
+ - every current apex DNS record, type, value/target, proxy state, and TTL;
+ - the current `www` record, target, proxy state, and TTL;
+ - the Secure_Tools GitHub Pages custom-domain and HTTPS-enforcement state;
+ - Hub Pages project production branch, stable hostname, deployment, custom domains, and
+ analytics/Functions/Workers state;
+ - Web Utilities Pages project, tools custom domain, deployment, and indexing-header state;
+ - active CAA records and certificate-authority restrictions.
+6. Store the snapshot in the H3.5 evidence record and explicitly identify the exact DNS values
+ to restore. Do not continue without a usable rollback record.
+7. Recheck existing production immediately before the first mutation. Any unexplained DNS,
+ TLS, route, asset, or functional regression is a no-go.
+
+## B. Normalize `www` before the apex move
+
+1. Create a Cloudflare zone/account-level Redirect Rule or Bulk Redirect for:
+
+ ```text
+ https://www.securetools.app/*
+ → 301 https://securetools.app/*
+ ```
+
+2. Configure the Cloudflare-proxied placeholder DNS record required by the selected redirect
+ mechanism. Pages `_redirects` cannot implement this domain-level redirect.
+3. Enable path and query-string preservation. Never send `www` to
+ `tools.securetools.app`.
+4. While the apex still serves the existing Web Utilities production, validate representative
+ `www` paths and queries return one 301 hop to the equivalent working apex URL.
+5. Confirm this removes the direct `www` dependency on GitHub Pages without changing the
+ apex origin. Roll back the `www` record and rule from the snapshot if validation fails.
+
+## C. Activate the Hub repository preparation
+
+1. Merge the reviewed H3.4B pull request using the repository's established merge method.
+2. Wait for the automatic `main` deployment to `secure-tools-hub`.
+3. Verify the immutable deployment and `secure-tools-hub-53i.pages.dev`:
+ - all 10 Hub routes and representative assets return 200;
+ - canonical, `og:url`, `og:image`, and `twitter:image` use
+ `https://securetools.app`;
+ - the Hub-only sitemap has 10 unique URLs and robots references it;
+ - stable and immutable Pages hostnames return
+ `X-Robots-Tag: noindex, nofollow`;
+ - all 18 legacy paths return a single 301 to the same path on
+ `tools.securetools.app`; and
+ - `/` renders the Hub and does not redirect.
+4. Do not attach the apex yet. A deployment or validation failure is a no-go.
+
+## D. Activate the Web Utilities SEO identity
+
+1. Merge Secure_Tools PR #73 only after step C passes.
+2. Wait for its automatic Cloudflare Pages deployment.
+3. Validate all 19 H3.1 routes and representative assets on
+ `https://tools.securetools.app`.
+4. Confirm the tools custom domain is indexable, all canonical and `og:url` values
+ self-reference the tools host, social images use that host, and its sitemap and robots
+ contain only Web Utilities URLs.
+5. Confirm stable and immutable `secure-tools-web-bridge.pages.dev` hostnames remain
+ `noindex, nofollow`.
+6. Confirm local file processing, privacy disclosures, and network behavior are unchanged.
+ Any mismatch blocks the apex move.
+
+## E. Attach the apex to the Hub
+
+1. Reconfirm the step A DNS snapshot, rollback values, existing GitHub Pages health, and CAA
+ compatibility.
+2. In Cloudflare use **Workers & Pages → secure-tools-hub → Custom domains → Set up a
+ domain**, then enter `securetools.app`.
+3. Because the apex is in a Cloudflare-managed zone, expect Pages association to change its
+ DNS relationship. Do not make unrelated DNS changes.
+4. Wait for the custom domain to report Active and for a valid certificate covering
+ `securetools.app`. If CAA blocks issuance, stop and use a separately reviewed CAA change;
+ do not weaken certificate policy ad hoc.
+5. Validate:
+ - `/` returns the Hub with 200 and no redirect;
+ - every Hub route, asset, sitemap, and robots response is correct;
+ - all 18 legacy paths return exactly one 301 to the identical
+ `tools.securetools.app` path;
+ - representative query strings survive the redirect;
+ - no Hub route collides with a redirect; and
+ - no loop, chain, mixed content, or certificate error exists.
+
+## F. Post-cutover isolation
+
+1. Revalidate the Hub apex, Hub Pages aliases, tools custom domain, tools Pages aliases, and
+ `www → apex` redirect.
+2. Confirm the apex is indexable and Pages aliases remain non-indexable.
+3. Confirm Web Utilities functionality and local-first network behavior on the tools host.
+4. Keep the old GitHub Pages workflow, custom-domain configuration, `CNAME`, and known-good
+ deployment available as rollback infrastructure where practical during stabilization.
+5. Record workflow runs, GitHub Deployments, immutable URLs, DNS state, headers, route matrix,
+ TLS evidence, and the final go/no-go decision.
+
+## G. Search activation
+
+Only after A–F pass:
+
+- submit `https://securetools.app/sitemap.xml` to the root property;
+- monitor `https://tools.securetools.app/` separately and submit its sitemap to the tools
+ URL-prefix property;
+- retain the domain property if already used for aggregate observation; and
+- do not use Change of Address for this partial migration.
+
+Search Console changes and ongoing migration monitoring belong to the later monitoring phase,
+not H3.4B.
+
+## Immediate rollback
+
+Rollback is triggered by a critical TLS, DNS, availability, route, redirect, canonical,
+privacy, or functional failure that cannot be corrected safely inside the change window.
+
+1. Stop additional activation and preserve evidence.
+2. Restore the snapshotted apex DNS relationship so `securetools.app` returns to the previous
+ GitHub Pages Web Utilities production.
+3. Restore the previous `www` DNS/redirect behavior if the normalized rule prevents the
+ known-good service from being reached.
+4. Confirm GitHub Pages custom-domain and HTTPS state match the snapshot.
+5. Revalidate production routes, assets, TLS, and representative functions.
+6. Revert or redeploy the Hub and Web Utilities SEO changes as needed to avoid conflicting
+ canonical, sitemap, robots, or indexing signals.
+7. Leave `tools.securetools.app` available on the parallel Cloudflare deployment unless it is
+ itself the failure source. Do not destroy the bridge merely to restore the apex.
+
+Rollback completion requires recorded DNS, TLS, HTTP, and functional evidence. Cleanup of old
+infrastructure is a later, separately reviewed decision.
diff --git a/docs/seo.md b/docs/seo.md
new file mode 100644
index 0000000..810d28e
--- /dev/null
+++ b/docs/seo.md
@@ -0,0 +1,55 @@
+# Hub search metadata
+
+Status: H3.4B preparation only. The pull request must remain unmerged until the coordinated
+H3.5 cutover.
+
+## Final canonical identity
+
+The final Hub origin is `https://securetools.app`. Each of the 10 public Hub routes has one
+self-referencing absolute canonical and matching `og:url`. Open Graph and X image metadata
+uses the existing 1200×630 PNG at
+`https://securetools.app/assets/brand/social-preview.png`.
+
+`404.html` is an error document, not a canonical Hub route. It is excluded from the sitemap
+and does not receive a canonical URL.
+
+## Crawler files
+
+- `public/sitemap.xml` lists exactly the 10 Hub canonical URLs.
+- `public/robots.txt` allows crawling and references
+ `https://securetools.app/sitemap.xml`.
+- No Web Utilities route, tools host, legacy redirect source, or Pages hostname appears in the
+ Hub sitemap.
+
+The static `public/_headers` contract applies
+`X-Robots-Tag: noindex, nofollow` only to:
+
+```text
+https://secure-tools-hub-53i.pages.dev/*
+https://:version.secure-tools-hub-53i.pages.dev/*
+```
+
+The future `securetools.app` custom domain does not match those patterns and remains
+indexable. No Worker or Pages Function is required.
+
+## Legacy path ownership
+
+`public/_redirects` contains exactly the 18 explicit 301 mappings in
+`docs/migrations/h3-url-map.csv`. Every destination is the same path on
+`https://tools.securetools.app`. The Hub root and all Hub routes are excluded; no wildcard
+can swallow future Hub content.
+
+These redirects become reachable on the apex only after H3.5 attaches the custom domain to
+the Hub Pages project. The H3.4B feature branch does not deploy or activate them.
+
+## Validation and search activation
+
+`scripts/validate-h3-hub-cutover.py` checks route metadata, social image dimensions,
+sitemap, robots, and Pages-alias isolation. `scripts/validate-h3-url-map.py` compares the
+redirect artifact directly with the H3.1 inventory and rejects duplicates, wildcards, wrong
+hosts, changed paths, non-301 status, root redirects, and Hub-route collisions.
+
+Search Console remains unchanged during preparation. After H3.5 HTTP, TLS, canonical, and
+redirect validation, submit the Hub sitemap to the root property and monitor Web Utilities
+through the tools URL-prefix property. Do not use Change of Address for this partial
+migration.