docs(pwa): add origin migration playbook
- commit
- cbdec3328861450f71a99a6c6193326f5585b5d1
- parent
- 13547cd3f6c9bfff129e022f03640cd66ef6bf33
- parent
- 7974a17fc23ca335208a85018c28364c557025ea
- author
- FelineStateMachine <dami.etoile@gmail.com>
| M | deno.json | 1 | + |
| M | docs/ | 4 | +- |
| A | docs/ | 105 | ++++++++++++++++++++++++++++++ |
| A | tools/ | 33 | +++++++++ |
@@ -27,7 +27,9 @@ flowchart LR
2727 lifecycle.2828 5. [Testing](testing.md) — run fast tests and the opt-in two-client offline convergence example.2929 6. [Deployment](deployment.md) — build, preview, customize, and host the static PWA.30-7. [Troubleshooting](troubleshooting.md) — diagnose common environment, browser, build, and test30+7. [Application-origin migration](application-origin-migration.md) — move a deployed app across a31+ browser security boundary with explicit recovery, verification, rollback, and retirement.32+8. [Troubleshooting](troubleshooting.md) — diagnose common environment, browser, build, and test3133 failures.3234 3335 The root [README](../README.md) provides the shortest product overview and command summary.@@ -0,0 +1,105 @@
1+# Application-origin migration2+3+Changing a production app from one origin to another creates a new browser security boundary. An4+HTTPS redirect moves navigation; it does not move the old origin's service workers, Cache Storage,5+IndexedDB, OPFS, permissions, installed-app identity, or WebAuthn credentials.6+7+This playbook keeps the old origin available while each user deliberately restores account access or8+imports a product-owned export, verifies the new origin, and only then removes the old installation.9+It never copies account secrets, passkeys, opaque IndexedDB/OPFS files, or browser permission state.10+11+## Choose the migration contract12+13+Record these decisions before deploying the new origin:14+15+| Decision | Required choice |16+| ----------------------- | -------------------------------------------------------------------------------- |17+| Old-origin availability | A dated compatibility window long enough for offline users to return |18+| Data path | Managed account sync/recovery, or a versioned product-owned export/import format |19+| Manifest identity | A deliberate new same-origin `id`, `start_url`, and `scope` |20+| Credentials | New stable hostname/RP ID and a new-origin enrollment/recovery ceremony |21+| Install UX | Whether old and new installations coexist and how each is labeled |22+| Retirement | Warning dates, final read-only date, rollback owner, and support path |23+24+Do not silently add the new hostname to `credentialOrigins`. A WebAuthn credential is scoped by its25+RP ID and origin checks; changing the permanent hostname is an identity decision, not a redirect26+configuration detail. Keep lofi's checks intact and require an explicit new-origin recovery or27+enrollment flow.28+29+## Pick one data path30+31+### Managed account sync and recovery32+33+1. On the old origin, reconnect and wait until the UI reports global durability for the latest34+ write.35+2. Make sure the user has a portable recovery phrase. An old-origin passkey alone cannot establish36+ the same RP ID on a different hostname.37+3. Open the new origin directly, create its fresh local browser storage, and restore the account38+ with the supported recovery ceremony.39+4. Wait for synced data to appear, then compare representative counts and named records.40+5. Make a harmless test edit on the new origin and verify global durability and another-client41+ convergence before treating the migration as complete.42+43+Never put an account secret, recovery phrase, or serialized Jazz/browser database in a URL,44+redirect, query parameter, cross-origin message, analytics event, or support log.45+46+### Product-owned export and import47+48+1. Export only a documented product schema from the old origin. Version it, bound its size, and omit49+ credentials, opaque storage records, cache entries, handles, and permission grants.50+2. On the new origin, validate the file as external input and show an import preview.51+3. Require explicit confirmation before writing it to the new local database.52+4. Compare representative counts and records, reload, go offline, and verify the imported data53+ again.54+5. Preserve the export until the compatibility window closes.55+56+The [file handling recipe](recipes/file-handler.md) provides a preview-only validation boundary, but57+an ordinary file picker remains the portable migration path.58+59+## Deployment sequence60+61+1. Deploy the new origin with its own manifest identity, icons, worker scope, database name, and62+ stable credential-origin configuration.63+2. Keep the old origin's application shell, worker assets, recovery/export UI, and support page64+ available. Do not blanket-redirect these paths during the compatibility window.65+3. Add old-origin messaging that links to the exact new HTTPS origin and explains that local data66+ and permissions do not move automatically.67+4. Let both installations coexist. Give the new installation distinct temporary labeling if the OS68+ would otherwise make them ambiguous.69+5. Ask for notifications, storage persistence, file/protocol associations, and other permissions70+ again only when the user reaches the relevant new-origin feature.71+6. After user verification, offer instructions to uninstall the old app. Never uninstall it or clear72+ its storage programmatically as part of migration.73+74+## Repeatable verification checklist75+76+Run this with a clean browser profile and with a profile containing representative production-like77+data. Retain dates and non-secret results with the release evidence.78+79+- [ ] `ORIGIN-01` Old origin opens online and offline with its retained worker/app shell.80+- [ ] `ORIGIN-02` New origin installs with the intended new manifest ID, scope, and display name.81+- [ ] `ORIGIN-03` New origin starts with separate IndexedDB, OPFS, Cache Storage, and permissions.82+- [ ] `ORIGIN-04` Chosen sync/recovery or export/import path transfers only product-owned data.83+- [ ] `ORIGIN-05` Representative counts and named records match before the old app is removed.84+- [ ] `ORIGIN-06` Reload and offline restart preserve verified data on the new origin.85+- [ ] `ORIGIN-07` A new-origin edit reaches its required durability and convergence state.86+- [ ] `ORIGIN-08` Old passkey behavior is rejected honestly; new recovery/enrollment succeeds.87+- [ ] `ORIGIN-09` Permissions and OS associations are requested or registered afresh as documented.88+- [ ] `ORIGIN-10` Old and new installations are distinguishable while they coexist.89+- [ ] `ORIGIN-11` Offline users returning during the window still receive migration instructions.90+- [ ] `ORIGIN-12` No secret or opaque browser-storage value appears in URLs, logs, or artifacts.91+92+## Partial migration and rollback93+94+Migration is per user and per device. Track `not started`, `restored/imported`, `verified`, and95+`old installation removed` separately; never infer completion from a redirect or a visit.96+97+If verification fails, stop new-origin writes when practical, keep the old origin authoritative, and98+preserve the user's export/recovery material. Fix the new deployment or importer, then repeat from a99+fresh new-origin profile. Do not merge divergent local datasets automatically unless the product has100+a tested domain-level reconciliation strategy.101+102+Delay retirement when offline users have not had a reasonable return window, recovery/export remains103+broken, or support cannot distinguish partial states. At retirement, keep a minimal non-sensitive104+status/support page for the announced period, revoke old deploy credentials, and retain rollback105+artifacts. Removing DNS or the old worker too early strands users whose only current copy is local.@@ -0,0 +1,33 @@
1+const path = new URL("../docs/application-origin-migration.md", import.meta.url);2+const source = await Deno.readTextFile(path);3+4+function assert(condition: unknown, message: string): asserts condition {5+ if (!condition) throw new Error(message);6+}7+8+Deno.test("origin migration playbook retains the complete repeatable safety contract", () => {9+ for (10+ const boundary of [11+ "service workers",12+ "Cache Storage",13+ "IndexedDB",14+ "OPFS",15+ "permissions",16+ "WebAuthn",17+ "manifest identity",18+ ]19+ ) {20+ assert(source.includes(boundary), `origin playbook omitted ${boundary}`);21+ }22+ for (let index = 1; index <= 12; index++) {23+ const id = `ORIGIN-${String(index).padStart(2, "0")}`;24+ assert(source.includes(`- [ ] \`${id}\``), `origin checklist omitted ${id}`);25+ }26+ assert(source.includes("## Partial migration and rollback"), "rollback section is missing");27+ assert(source.includes("offline users"), "offline-user retirement case is missing");28+ assert(29+ source.includes("Never put an account secret") &&30+ source.includes("never copies account secrets"),31+ "cross-origin secret-copy prohibition is missing",32+ );33+});