Skip to content

[Personal-WP] Add direct Personal WP desktop access#3758

Open
akirk wants to merge 38 commits into
WordPress:trunkfrom
akirk:personal-wp-desktop-webrtc-trunk
Open

[Personal-WP] Add direct Personal WP desktop access#3758
akirk wants to merge 38 commits into
WordPress:trunkfrom
akirk:personal-wp-desktop-webrtc-trunk

Conversation

@akirk

@akirk akirk commented Jun 6, 2026

Copy link
Copy Markdown
Member

Demo

mywp-connect.mp4

What It Does

This is a more privacy friendly alternative to #3746. It keeps the same product goal: let someone open the Personal WP instance running on their phone from a desktop browser while the phone remains nearby and connected.

The main difference is transport. #3746 proxies WordPress HTTP requests and responses through the PHP relay. This PR uses the PHP relay only for rendezvous and WebRTC signaling, then sends WordPress HTTP traffic over a direct browser-to-browser WebRTC DataChannel.

Related PRs:

Important Implementation Notes

  • The relay is signaling-only. WordPress HTTP request/response bodies go over the WebRTC DataChannel, not through PHP/MySQL.
  • Relay storage is normalized across sessions, signals, and guest heartbeats. There is no session-level JSON payload blob.
  • Signal seq values are only used as polling cursors/order markers.
  • Signal cleanup is time-based via SIGNAL_RETENTION_MS, not modulo slots or a retained sequence tail.

User Flow

On the phone:

  1. Open Personal WP.
  2. Open Site Tools.
  3. In the Use on desktop section, start desktop access.
  4. The phone shows /connect plus a six-digit access code.
  5. Keep the phone tab open while using the desktop.

On the desktop:

  1. Open /connect on the same Personal WP deployment.
  2. Enter the six-digit code from the phone.
  3. The desktop opens the viewer and establishes a direct WebRTC DataChannel with the phone.
  4. The desktop shows a two-digit verification code, for example 42.
  5. The phone shows an approval prompt. Enter the desktop verification code and tap Allow, or press Enter on the phone keyboard.
  6. The desktop iframe loads WordPress through the phone runtime.
  7. Use Download backup in the desktop viewer header to request and download a zip backup from the running phone site over the WebRTC tunnel.

While connected, the desktop page renders the WordPress instance that is still running on the phone. The visible desktop URL is normalized to /connect while connecting, then cleaned up to paths like /connect/wp-admin/ once the iframe navigates so the user can tell where they are without seeing the long session id. These are currently transient viewer URLs; reload is not expected to restore the session.

Architecture

The PHP relay is only a rendezvous/signaling service. It creates short-lived sessions, resolves six-digit access codes, records host/guest heartbeats, and stores small WebRTC signaling messages while the browser peers connect.

It does not expose a /relay/:session/request/* HTTP proxy in this branch.

The request path is:

  1. Desktop iframe requests /scope:default/....
  2. The Playground service worker sees the scoped request.
  3. The service worker posts a serialized request to the desktop viewer page.
  4. The desktop viewer sends that request over a WebRTC DataChannel.
  5. The phone receives it and calls playgroundClient.request(...).
  6. The phone serializes the response and sends it back over the DataChannel.
  7. The desktop service worker builds a Response for the iframe.

So WordPress request/response bodies are intended to stay out of PHP and MySQL in this approach.

Relay Storage

The relay now uses three normalized MySQL tables instead of a session-level JSON blob.

mywp_desktop_access_sessions

Stores session metadata and the phone host heartbeat:

CREATE TABLE mywp_desktop_access_sessions (
  session_id varchar(64) NOT NULL PRIMARY KEY,
  access_code varchar(7) NOT NULL UNIQUE,
  created_at_ms bigint unsigned NOT NULL,
  last_activity_ms bigint unsigned NOT NULL,
  last_host_seen_at_ms bigint unsigned NOT NULL DEFAULT 0,
  host_connected tinyint(1) NOT NULL DEFAULT 0,
  KEY access_code_idx (access_code),
  KEY last_activity_idx (last_activity_ms)
);

mywp_desktop_access_signals

Stores ordered WebRTC/control messages addressed from one peer to the other:

CREATE TABLE mywp_desktop_access_signals (
  seq bigint unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
  session_id varchar(64) NOT NULL,
  from_peer varchar(8) NOT NULL,
  to_peer varchar(8) NOT NULL,
  signal_type varchar(16) NOT NULL,
  signal_data json NULL,
  created_at_ms bigint unsigned NOT NULL,
  KEY session_to_seq_idx (session_id, to_peer, seq),
  KEY session_created_idx (session_id, created_at_ms),
  KEY created_idx (created_at_ms)
);

seq is only the delivery cursor/order marker for polling. Cleanup is time-based: old signal rows are deleted after SIGNAL_RETENTION_MS instead of keeping a sequence-number tail.

mywp_desktop_access_guests

Stores desktop viewer heartbeats:

CREATE TABLE mywp_desktop_access_guests (
  session_id varchar(64) NOT NULL,
  guest_id varchar(128) NOT NULL,
  last_seen_at_ms bigint unsigned NOT NULL,
  PRIMARY KEY (session_id, guest_id),
  KEY last_seen_idx (last_seen_at_ms)
);

The relay expects these tables to already exist. It does not create or migrate
database schema at runtime.

Pairing and Approval

The six-digit access code is only the rendezvous step. It lets a desktop find the current phone session, but it is not enough to access WordPress.

After the WebRTC channel opens:

  • the desktop generates and displays a two-digit verification code;
  • the desktop sends that code to the phone over the DataChannel;
  • the phone requires the user to enter that exact code before approving;
  • the phone rejects WordPress requests until approval is complete.

This is meant to protect against brute-forcing the six-digit rendezvous code. A guessed code can reach the approval gate, but the user still has to confirm the desktop from the phone.

Desktop Viewer Details

The desktop viewer registers the Playground service worker at /sw.js with root scope and verifies that /scope:default/ is controlled before loading the iframe.

The iframe starts at about:blank and is only navigated once the service worker and DataChannel are ready. This avoids accidentally booting a local Playground instance in the desktop iframe.

The viewer preserves the loaded iframe during reconnects. Reconnect state is shown in the header/debug UI instead of blocking the already-loaded page.

A compact Debug disclosure in the header shows relay diagnostics such as:

  • service worker mapping/control state;
  • WebRTC peer/DataChannel state;
  • iframe load state;
  • intercepted/request/pending counts;
  • last request and last error.

The status pill also exposes the same diagnostics via its title attribute.

Difference From #3746

#3746:

  • Adds a PHP HTTP relay endpoint for WordPress requests.
  • Stores queued HTTP request/response payloads on the relay temporarily.
  • Needs relay request handling, response handling, batching/backpressure work, and URL/redirect handling.
  • Works even when phone and desktop cannot establish a direct peer connection, as long as both can reach the relay.

This PR:

  • Is based directly on trunk.
  • Uses a compact signaling-only PHP relay backed by normalized MySQL tables.
  • Does not include the old request relay, request batching, URL rewriting, or TunnelHost HTTP polling implementation.
  • Sends WordPress HTTP traffic over WebRTC DataChannel.
  • Requires phone approval after desktop pairing.
  • Has a smaller diff than the stacked HTTP relay branch.

Privacy and Data Handling

This approach reduces what the relay sees. MySQL temporarily stores:

  • session ids and six-digit access codes;
  • phone host heartbeat state;
  • desktop viewer heartbeat ids;
  • WebRTC signaling/control messages such as offer, answer, ICE candidates, approval, and disconnect.

It should not store WordPress HTTP bodies, cookies, admin requests, HTML, CSS, JS, media, form submissions, or backup zip contents. The desktop and phone exchange those request/response payloads and desktop-requested backup chunks directly over the WebRTC DataChannel.

Server Configuration

The relay reads DB configuration from these env/server variables, in order:

  • PLAYGROUND_RELAY_DB_HOST, MYWP_DB_HOST, DB_HOST
  • PLAYGROUND_RELAY_DB_USER, MYWP_DB_USER, DB_USER
  • PLAYGROUND_RELAY_DB_PASSWORD, MYWP_DB_PASSWORD, DB_PASSWORD
  • PLAYGROUND_RELAY_DB_NAME, MYWP_DB_NAME, DB_NAME
  • PLAYGROUND_RELAY_DB_PORT, MYWP_DB_PORT, DB_PORT

PLAYGROUND_RELAY_PUBLIC_BASE_URL may be set to force generated desktop share URLs.

Caveats

This is a prototype.

The WebRTC peer connection currently uses no STUN or TURN servers:

iceServers: []

That keeps the first version focused on same-network/direct connectivity. It may fail on networks where the browsers cannot discover a usable direct route. Adding STUN would likely improve direct connection setup. Adding TURN would improve reliability further, but TURN would relay encrypted WebRTC traffic and change the privacy tradeoff.

The phone tab must stay open. The desktop is a viewer/control surface for the running phone Playground runtime.

The clean desktop URL, such as /connect/wp-admin/, is currently cosmetic/transient. Reloading it is not expected to restore the desktop session.

Installing apps via postMessage from the desktop viewer is still reported as unsupported for now.

Validation

Ran during this branch:

  • php -l packages/playground/personal-wp/public/relay.php
  • npm exec nx typecheck playground-personal-wp
  • npm exec nx typecheck playground-remote
  • git diff --check

I did not run the full build or deployment scripts.

@akirk akirk force-pushed the personal-wp-desktop-webrtc-trunk branch from aa0319e to 07016be Compare June 6, 2026 13:12
@akirk akirk force-pushed the personal-wp-desktop-webrtc-trunk branch 2 times, most recently from 927b078 to cb50804 Compare June 7, 2026 17:27
@akirk akirk changed the title Prototype direct Personal WP desktop access [Personal-WP] Add direct Personal WP desktop access Jun 8, 2026
@akirk akirk marked this pull request as ready for review June 8, 2026 12:41
@akirk akirk requested review from a team, brandonpayton and Copilot June 8, 2026 12:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Implements “Personal WP desktop access” via a signaling-only PHP relay plus a direct browser-to-browser WebRTC DataChannel tunnel, enabling desktop viewing/control of the phone-hosted Playground runtime.

Changes:

  • Add a service-worker “desktop relay” request bridge for /scope:default/* traffic.
  • Add phone (host) and desktop (guest) tunnel implementations using WebRTC DataChannels, including approval + backup download over the tunnel.
  • Add a minimal PHP/MySQL relay for session rendezvous + signaling (with Apache rewrite routing).

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
tsconfig.base.json Adds a TS path alias for importing the remote service worker entry.
packages/playground/remote/src/lib/desktop-relay.ts Implements service-worker-side mapping, probing, and request forwarding to the desktop client.
packages/playground/remote/src/lib/desktop-relay.spec.ts Adds unit tests for header/body serialization helpers used by the desktop relay.
packages/playground/remote/service-worker.ts Wires desktop relay messaging + fetch interception into the remote service worker.
packages/playground/personal-wp/vite.config.ts Configures worker bundling so the imported service worker builds to sw.js.
packages/playground/personal-wp/src/main.tsx Routes /connect and ?share= viewer flows outside the normal app shell.
packages/playground/personal-wp/src/lib/desktop-access-tunnel-utils.ts Adds shared helpers for attempt scoping, verification codes, filenames, and ICE candidate buffering.
packages/playground/personal-wp/src/lib/desktop-access-service.ts Adds a thin app-facing service wrapper to start/stop/approve desktop access.
packages/playground/personal-wp/src/lib/desktop-access-direct-tunnel.ts Implements WebRTC signaling + DataChannel HTTP tunneling and backup transfer (host + guest).
packages/playground/personal-wp/src/lib/desktop-access-direct-tunnel.spec.ts Adds unit tests for desktop access helper utilities (attempt scoping, codes, filenames, ICE buffering).
packages/playground/personal-wp/src/components/site-manager/site-info-panel/style.module.css Adds UI styles for desktop access controls + diagnostics within Site Tools.
packages/playground/personal-wp/src/components/site-manager/site-info-panel/index.tsx Adds the “Use on desktop” section, start/stop/share/copy, and approval UI.
packages/playground/personal-wp/src/components/desktop-access-viewer/style.module.css Adds desktop viewer layout styles, status/diagnostics UI, and overlays.
packages/playground/personal-wp/src/components/desktop-access-viewer/index.tsx Implements the desktop viewer: SW registration, request bridging, WebRTC guest, and backup download.
packages/playground/personal-wp/src/components/desktop-access-connect/style.module.css Adds styling for the /connect access-code entry page.
packages/playground/personal-wp/src/components/desktop-access-connect/index.tsx Implements /connect access-code resolution and route detection helpers.
packages/playground/personal-wp/src/components/desktop-access-connect/index.spec.ts Adds tests for /connect route + code normalization/formatting helpers.
packages/playground/personal-wp/public/relay.php Adds the signaling-only PHP relay backed by normalized MySQL tables.
packages/playground/personal-wp/.htaccess Routes /relay/* requests to relay.php.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +4 to +5
* The phone keeps running the real Playground runtime. Desktop browsers only
* send HTTP requests through the relay and render the responses.
Comment on lines +240 to +245
const desktopRelayMapping = getDesktopRelayMapping(scope);
if (desktopRelayMapping) {
return event.respondWith(
handleDesktopRelayRequest(event, desktopRelayMapping)
);
}
Comment on lines +627 to +635
private queueDataChannelMessage(data: unknown, attemptId: string): void {
if (typeof data !== 'string' || !this.dataChannel) {
return;
}
const request = JSON.parse(data) as DataChannelHostMessage;
if (isDataChannelControlMessage(request)) {
this.handleDataChannelControlMessage(request, attemptId);
return;
}
Comment on lines +1539 to +1549
channel.onmessage = (event) => {
if (typeof event.data !== 'string') {
return;
}
const response = JSON.parse(event.data) as DataChannelGuestMessage;
if (
isDataChannelControlMessage(response) &&
response.attemptId !== attemptId
) {
return;
}
Comment on lines +814 to +820
await this.sendDataChannelResponse({
type: 'response',
requestId: request.requestId,
status: phpResponse.httpStatusCode,
headers: responseHeaders,
body: uint8ArrayToBase64(phpResponse.bytes),
});
)
VALUES (?, ?, ?, ?, ?, ?)'
);
$stmt->bind_param('sssssi', $sessionId, $from, $to, $type, $encodedData, $now);
Comment on lines +347 to +353
function normalizeVerificationCode(value: string): string {
return value.replace(/\D+/g, '').slice(0, 2);
}

function formatVerificationCode(value: string): string {
return normalizeVerificationCode(value);
}
Comment on lines +6 to +7
// @ts-ignore
import serviceWorkerPath from '@wp-playground/remote/service-worker?worker&url';
* Phone-side direct tunnel. The relay is only used to exchange WebRTC
* signaling messages; WordPress HTTP requests are handled over the data channel.
*/
export class DirectTunnelHost {
return fallback;
}

export class DirectTunnelGuest {
@akirk akirk force-pushed the personal-wp-desktop-webrtc-trunk branch from 07a1cc4 to 9a8b364 Compare June 8, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants