Sign in
EN

Embedding a JumPHP application on a partner's site

The kit and the WordPress plugin are in the JumPHP repository: https://git.jumphp.net/jumphp/jumPhp/src/branch/main/tools/embed

An application published on the playground (https://jumphp.net) can appear on another site as one of its own pages: no iframe, no CORS, same origin. Search engines see the HTML on the partner's site (a canonical to its address), and the application gets the visitor's provenance (the site they came from) and the fields of the form that sent them.

Real case: caramail-chat on https://caramail.tv: a login form at the top of every page (a block served by the application, updated with it) that sends to https://caramail.tv/chat/, where the application runs as if it were hosted there.

browser ──► caramail.tv/chat/...  ──(signed request)──►  jumphp.net/api/embed/caramail-chat/app/...
            partner's server       ◄──── answer ───────   (rewritten page, engine, files)
            (jumphp-embed.php kit)
browser ──► wss://jumphp.net/tcp (TCP bridge, direct: a WebSocket has no CORS)

1. The application (the project)

config/project.yaml:

project:
  embed:
    blocks: [login]                       # site/blocks/login.html: HTML fragments the partner shows
    params: [nickname, age, city, sex]    # the form fields passed to the application (first scene)
  • A block is an HTML fragment (site/blocks/<name>.html), no <html>: a form, a banner, a list of rooms… {{BASE}} becomes the application's address on the partner's site (https://caramail.tv/chat), {{SITE}} its domain, {{APP}} the slug. Its pictures: {{BASE}}/site/images/logo.png (the files of site/ are relayed too).
    <form class="jumphp-login" method="get" action="{{BASE}}/">
      <input name="nickname" required maxlength="20" placeholder="Nickname">
      <button>Enter the chat</button>
    </form>
    
  • The params reach enter() / loaded() of the first scene, over project.yaml's start_params: only the names declared in embed.params, as strings, and always site = the partner's domain (set by the playground from the signature: never what the visitor could type in the address).
  • The form may be method="get" (fields in the address) or method="post" (the kit relays them as they are).
  • A terminal application (kind: cli) cannot be embedded yet. Games, graphic apps (kind: gui) and desktop apps in the browser (ui: webview) can.

How the engine gets the params: the page sets window.JUMPHP_GAME.start_params (→ constant JUMPHP_START_PARAMS, merged by Engine::startParams() over start_params) and tcp_ticket_url (→ JUMPHP_TCP_URL) in the engines released after JumPHP 0.5.53; for every engine version, the playground also serves that page a config/project.yaml that already holds these start_params and this tcp_ticket_url (the params travel sealed, HMAC, from the page to its manifest).

2. The playground (the project's owner)

Studio → the project → On a partner's site: domain (caramail.tv), base path (/chat) → Add. The studio gives:

  • the site's identifier (JUMPHP_EMBED_SITE=es_…, public);
  • the shared secret (JUMPHP_EMBED_SECRET=jes_…, kept encrypted, shown to the owner only).

The owner can disable / enable it, renew the secret (the old one stops at once) or delete it. The administrator sees every partner site (Administration → Partner sites and domains) and can cut one (with a reason): the owner cannot restore it.

3. The partner: the kit

tools/embed/jumphp-embed.php: one PHP file (7.4+, curl), no dependency. It:

  • serves the application under the base path: /chat/ = the page, and everything it loads (engine/<version>/… the shared engine and its wasm, manifest.json, files/…, private/…, embed-config.yaml, site/…);
  • signs each request and relays it streamed (big files never sit in memory), with Range, If-None-Match, and the playground's gzip passed through;
  • keeps immutable files on disk (?v=<hash> addresses and the engine's versioned folder, up to 64 MB) and then serves them itself (Range included);
  • keeps the blocks on disk (ttl, 300 s by default), revalidates them with their ETag, and returns the last copy when the playground does not answer;
  • relays the TCP bridge ticket (/chat/api/tcp/ticket) and the runtime wake-up (/chat/api/runtime/wake) with the visitor's address (X-JumPHP-Client-IP);
  • never forwards the visitor's cookies to the playground, nor the playground's cookies or X-Robots-Tag to the visitor; adds X-Content-Type-Options, Referrer-Policy, X-Frame-Options: SAMEORIGIN.

Settings (an array or environment variables): server / JUMPHP_EMBED_SERVER (https://jumphp.net), app / JUMPHP_EMBED_APP, site / JUMPHP_EMBED_SITE, secret / JUMPHP_EMBED_SECRET (never in git nor in a public file), base / JUMPHP_EMBED_BASE, cache / JUMPHP_EMBED_CACHE, ttl / JUMPHP_EMBED_TTL.

require '/srv/jumphp-embed/jumphp-embed.php';
$jumphp = JumphpEmbed::fromConfig(['app' => 'caramail-chat', 'base' => '/chat']);   // site, secret: the environment
if ($jumphp->serve()) { exit; }          // /chat/...: answered; otherwise the site goes on
echo $jumphp->block('login');            // in a page

Ready examples: tools/embed/examples/nginx.conf, tools/embed/examples/Caddyfile, tools/embed/examples/front.php. Fastest: nginx / Caddy send /chat/ straight to the kit without loading the CMS. Its tests: tools/embed/tests/test.sh (Docker).

WordPress

tools/embed/wordpress/build.sh makes jumphp-embed.zip (Plugins → Add → Upload). Settings → JumPHP Embed: the playground's address and one line per application (slug, identifier, secret, base path). The secret can stay out of the database: define('JUMPHP_EMBED_SECRET_CARAMAIL_CHAT', 'jes_…'); in wp-config.php.

  • Shortcode: [jumphp_block app="caramail-chat" block="login"] (a page, a widget, or do_shortcode() in the theme's header).
  • The base path (/chat/…) is taken at plugins_loaded, before the theme and WordPress's query. A page cache must exclude /chat/.

4. The TCP bridge

A page asks its ticket from its own origin by default (/api/tcp/ticket). On the partner's site that origin is caramail.tv, so the embedded page asks https://caramail.tv/chat/api/tcp/ticket (tcp_ticket_url), which the kit relays, signed. The ticket names the bridge wss://jumphp.net/tcp, opened directly by the browser (a WebSocket is not subject to CORS), with the same administrator approvals (docs/tcp.md). Every HTTP request stays same-origin, and the tickets API is not opened to every site's CORS.

5. The API (to write another kit)

Headers of every request: X-JumPHP-Site (es_…), X-JumPHP-Time (Unix time, refused beyond 300 s from the playground's clock), X-JumPHP-Signature = hex HMAC-SHA256(secret, METHOD\npath\nquery\ntime\nsha256hex(body)), path and query as sent (encoded, query without ?), compared in constant time. Test vector: secret s3cret, GET /api/embed/a/block/login, time 1000, no query nor body → hash_hmac('sha256', "GET\n/api/embed/a/block/login\n\n1000\n" . hash('sha256', ''), 's3cret').

Request Answer
GET /api/embed/<slug>/block/<name> the block, ETag, Cache-Control: public, max-age=300, 304 on If-None-Match
GET|POST /api/embed/<slug>/app/ the rewritten page (<base>, relative engine and files, canonical / og:url on the partner); query and POST body give the params
GET /api/embed/<slug>/app/<path> manifest.json, embed-config.yaml, files/…, private/…, packages/…, app/…, engine/<version>/…, site/…
POST /api/embed/<slug>/tcp/ticket the bridge ticket (X-JumPHP-Client-IP: the visitor's address)
POST /api/embed/<slug>/runtime/wake the runtime wake-up

Errors as JSON {"error": "..."}: 401 (signature missing, wrong, too old, unknown site), 403 (off, or cut by the administrator), 404 (not published, undeclared block, missing file), 405.