Skip to content

feat: migrate browser data from the legacy web origin to web.phcode.dev - #3130

Merged
abose merged 3 commits into
mainfrom
ai
Aug 27, 2026
Merged

feat: migrate browser data from the legacy web origin to web.phcode.dev#3130
abose merged 3 commits into
mainfrom
ai

Conversation

@abose

@abose abose commented Aug 22, 2026

Copy link
Copy Markdown
Member

Browser storage is per-origin, so a user landing on web.phcode.dev sees an empty editor while their projects, preferences, theme and extensions stay behind on the old origin, invisible to the new one.

On the origin being retired, every boot announces the move with a live countdown to the sunset date. On web.phcode.dev, a hidden iframe pointed at the old origin reports whether anything is worth moving. If there is nothing, absolutely nothing happens - no dialog, no interruption, normal boot. If there is, a progress modal runs the transfer once and sets a flag so it never runs again; after that it only happens if the user asks from Help > Migrate My Data.

This works at all because phcode.dev, staging.phcode.dev and web.phcode.dev are the same site (eTLD+1). Browsers partition third party storage by site rather than by origin, so the helper frame is same-site and reads real unpartitioned IndexedDB. Had the new home been on a different registrable domain this would have silently reported "nothing to migrate" everywhere.

src/migrateAssist.html is deliberately standalone. It loads only virtualfs.js and jszip.js rather than booting Phoenix, and zips one top level folder at a time so peak memory is one folder rather than the whole filesystem. The receiving side hands each zip to ZipUtils.unzipBinDataToLocation, which writes without unlinking first, so files from the old origin win on collision while files that only exist on the new side survive. That is the collision rule we want, with no extra code.

Extensions are copied rather than reinstalled from the registry. Both origins serve the byte identical build, so a folder that worked on one is compatible with the other, and copying also preserves extensions that were sideloaded or have since been unpublished.

That page hands out the user's entire browser filesystem to whoever embeds it, so the origin allowlist is exact match only, checked on every message and not just the handshake, replies always go to the validated origin rather than "*", and reads are confined to three known roots.

Safari and iOS are out of scope by product decision, not because the mechanism would fail there. Those users still get the dialog, with a line asking them to download projects manually, rather than losing data without being told.

Android and ChromeOS users launched from our Trusted Web Activity are offered an app update instead of the new site, because the shipped APK only trusts phcode.dev and navigating it elsewhere surfaces a browser URL bar inside what looks like an app. "Stay here" is a real choice there: managed school fleets can have the Play Store blocked entirely, so the update button may be a dead end through no fault of the user.

The legacy origin is staging.phcode.dev while this is validated end to end. Switching it to phcode.dev is a one line change in constants.js.

abose added 3 commits August 22, 2026 23:36
Browser storage is per-origin, so a user landing on web.phcode.dev sees an
empty editor while their projects, preferences, theme and extensions stay
behind on the old origin, invisible to the new one.

On the origin being retired, every boot announces the move with a live
countdown to the sunset date. On web.phcode.dev, a hidden iframe pointed at
the old origin reports whether anything is worth moving. If there is nothing,
absolutely nothing happens - no dialog, no interruption, normal boot. If there
is, a progress modal runs the transfer once and sets a flag so it never runs
again; after that it only happens if the user asks from Help > Migrate My Data.

This works at all because phcode.dev, staging.phcode.dev and web.phcode.dev are
the same site (eTLD+1). Browsers partition third party storage by site rather
than by origin, so the helper frame is same-site and reads real unpartitioned
IndexedDB. Had the new home been on a different registrable domain this would
have silently reported "nothing to migrate" everywhere.

src/migrateAssist.html is deliberately standalone. It loads only virtualfs.js
and jszip.js rather than booting Phoenix, and zips one top level folder at a
time so peak memory is one folder rather than the whole filesystem. The
receiving side hands each zip to ZipUtils.unzipBinDataToLocation, which writes
without unlinking first, so files from the old origin win on collision while
files that only exist on the new side survive. That is the collision rule we
want, with no extra code.

Extensions are copied rather than reinstalled from the registry. Both origins
serve the byte identical build, so a folder that worked on one is compatible
with the other, and copying also preserves extensions that were sideloaded or
have since been unpublished.

That page hands out the user's entire browser filesystem to whoever embeds it,
so the origin allowlist is exact match only, checked on every message and not
just the handshake, replies always go to the validated origin rather than "*",
and reads are confined to three known roots.

Safari and iOS are out of scope by product decision, not because the mechanism
would fail there. Those users still get the dialog, with a line asking them to
download projects manually, rather than losing data without being told.

Android and ChromeOS users launched from our Trusted Web Activity are offered
an app update instead of the new site, because the shipped APK only trusts
phcode.dev and navigating it elsewhere surfaces a browser URL bar inside what
looks like an app. "Stay here" is a real choice there: managed school fleets
can have the Play Store blocked entirely, so the update button may be a dead
end through no fault of the user.

The legacy origin is staging.phcode.dev while this is validated end to end.
Switching it to phcode.dev is a one line change in constants.js.
The suite failed in the Electron runner. The cause was the test, not the code:
the desktop shell's origin is phtauri://localhost, which migrateAssist.html
refuses, so the handshake never completed and the helper page specs sat there
until awaitsFor timed out.

That refusal is correct and worth keeping. The migration is browser only and a
native shell has no business reading a web origin's filesystem, so adding
phtauri://localhost to the allowlist would trade a real security boundary for
specs that assert nothing useful on desktop.

Early return on Phoenix.isNativeApp instead, the same shape already used by
Extn-Git-integ-test.js. The suite now registers nothing at all in the desktop
runner and is unchanged in browsers.
The suite failed in CI at the round trip spec: "Expected undefined to be
truthy", then a TypeError reading .id of undefined.

The spec took scan.bundles[0], so it silently depended on the machine already
having projects in /fs/local. That held on a developer machine and did not in a
fresh CI checkout, where the VFS is empty and the scan correctly returned no
bundles at all. The suite did have a seeded folder, but under /temp, which is
not one of the roots migrateAssist.html will serve, so it never appeared in a
scan. The same spec asserted that /temp is refused, which should have been the
clue.

Seeds a project under /fs/local instead, which is a served root, and looks the
bundle up by destination rather than by position. The seed carries a nested
file and a binary file containing NUL and 0xFF, so the round trip now also
proves paths keep their shape and bytes survive intact rather than being
mangled through a text decode. hasData is asserted too, which is meaningful now
that the suite controls whether there is anything to migrate.

Verified against a clean browser profile, which is the state CI starts in: the
previous spec fails there and this one passes, and the full unit category is
2730 of 2730.
@sonarqubecloud

Copy link
Copy Markdown

@abose
abose merged commit 630ee68 into main Aug 27, 2026
15 of 21 checks passed
@abose
abose deleted the ai branch August 27, 2026 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant