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 ofsite/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'sstart_params: only the names declared inembed.params, as strings, and alwayssite= 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) ormethod="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-Tagto the visitor; addsX-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, ordo_shortcode()in the theme's header). - The base path (
/chat/…) is taken atplugins_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.