# Browser hosting and account saves Hackemist hosts prebuilt browser applications. It serves HTML, JavaScript, CSS, media, fonts, JSON, WASM, engine data packs, binary assets, GLTF/GLB models, texture formats and shader sources. It does not install packages or execute submitted server processes. Apps can connect directly to approved external backends over HTTPS or WSS. ## Choose a runtime profile Site-wide pointer lock, Web Workers, shared-memory WASM and immersive WebXR are enabled by default. Admin → Site settings → 3D and immersive apps can switch each off for the whole platform. They work automatically without per-app approval; workers and WebXR select a dedicated player/project origin when isolated hosting is configured, and shared memory also requires enabled workers. Global changes revoke active launches so new documents use the current browser permissions. When dedicated hosting is unavailable, or automatic workers/XR are disabled, a restricted app can keep an opaque iframe origin and local bundle assets. This fallback blocks native browser storage, workers, nested frames, forms and external network access. Mouse capture follows its independent global switch. Many canvas/WebGL/static apps work in this profile. Creators can also choose supported isolated origins and automatic saves through Cloud saves or MCP `runtime_options_update` when creator options and safe wildcard runtime hosting are available. Browser compatibility shows the effective graphics permissions separately from stored app options. Its hostname is derived from the player, project and effective policy, on the separate runtime domain. Different accounts, projects and policies get different browser origins. The server checks the launch capability, project/player hostname, version and current access before serving assets. Policy changes revoke existing launches and rotate the origin so old workers cannot control a launch with newly restricted permissions. Confirmed automatic saves reload into the new origin; browser-only IndexedDB/caches are not migrated. The isolated profile supports native localStorage/sessionStorage, IndexedDB, globally enabled workers/shared memory, managed service-worker caching, and approved embedded app pages. API/WebSocket connections, external assets, frames and forms each use exact-origin allowlists. Managed caching, embedded pages and external destinations retain administrator review; direct per-app administrator changes require password confirmation. Global graphics switches apply regardless of legacy per-app flags. Creator changes to supported saves and first-party rooms preserve other permissions and are audited. See [creator runtime options and external backends](/docs/connections.md). Production needs wildcard runtime DNS, routing and TLS before enabling `RUNTIME_SUBDOMAINS=true`. Local Docker uses `runtime.localhost:8001` and its subdomains, which Chrome resolves to loopback. The Dokploy guide (operator repository reference: `GITHUB_AND_DOKPLOY.md`) includes a routing overlay and TLS requirements. Native browser storage availability can still depend on browser privacy settings. A dedicated sibling runtime such as `apps.hackemist.com` is supported with `RUNTIME_ALLOW_SAME_SITE=true`. Account cookies use the production `__Host-` prefix; account services reject credentialed sibling-origin requests, and the gateway removes parent-cookie debris. The runtime and realtime receive no account cookies or Authorization headers. A separate registrable domain still gives a stronger default browser boundary. See single-domain deployment (operator repository reference: `SINGLE_DOMAIN_DEPLOYMENT.md`) before enabling this mode. ## Save progress by account There are two independent save channels: | Save channel | Storage | App work required | | --- | --- | --- | | Explicit SDK JSON | Account + project, with revision checks; 64 KiB | Call `Hackemist.loadProgress()` and `saveProgress(data)` | | Automatic browser save | Account + project, with revision checks; 5 MiB | Existing localStorage writes are observed in an approved isolated profile | Automatic saves preload the stored dictionary before app scripts execute, observe set/remove/clear operations, and poll for property assignments. Confirmed writes update the player status. A stale tab cannot overwrite a newer revision; it must reopen before saving. If browser quota prevents a full restore, existing local data is restored and automatic sync stops, leaving the confirmed cloud save unchanged. Legacy UTF-16 save strings are preserved. Saves are included in account exports and encrypted backups, and deleted with the account. Guests get browser persistence without account cloud sync. JavaScript `document.cookie` is supported through an app-local storage jar, because third-party HTTP cookies may be blocked by browser privacy policy. These app cookies can sync with automatic saves. Domain, server authentication and HTTP-only cookie semantics are not emulated; external backend sessions remain subject to the browser's actual cookie/CORS rules. IndexedDB and service-worker caches persist in that browser origin but do not automatically sync to other devices. Memory-only game state requires an app save integration. Automatic saves run periodically and are best effort when a tab closes or loses network access. A game should still save checkpoints frequently. A browser/profile clearing operation removes local caches; signed-in automatic saves can restore confirmed localStorage data. They cannot reconstruct unsaved memory or synchronize arbitrary IndexedDB schemas. ## External services Enter exact origins such as `https://api.example.com` and `wss://multiplayer.example.com`. HTTPS/WSS is required in production. Wildcards, credentials, URL paths, control/runtime origins and literal private-network endpoints are rejected. Development can use explicit local fixture origins. The browser contacts the approved service directly. That service must support the appropriate CORS/authentication behavior. Hackemist does not forward private API secrets, proxy arbitrary URLs, run uploaded Node/Python servers, or supply each app with a server database. Keep backend secrets on your separately deployed backend. Approved destinations can receive the app's input and saves, so review them before granting access. Cross-origin isolation for threaded WASM requires matching browser headers and permission delegation. External frames/assets can be rejected when their providers do not support the necessary CORS/COEP/CORP policy. This preserves isolation rather than weakening it to make a provider load. The deployment follows [browser cross-origin isolation](https://developer.mozilla.org/en-US/docs/Web/API/Window/crossOriginIsolated) and [service-worker scope rules](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register). Service-worker support is a platform-managed asset cache, not execution of uploaded service-worker handlers. Arbitrary handlers could synthesize documents without CSP and bypass network restrictions. The managed worker never replaces navigation/HTML responses, retains server security headers, and caches only assets inside the current `/run//` directory. Apps can await `Hackemist.serviceWorkerReady`; custom `navigator.serviceWorker.register('sw.js')` requests are refused. Cached/downloaded code cannot be erased by revoking server access; offline cache is not DRM. ## Package games and graphics apps Put `index.html` at the ZIP root and include all needed local assets. Relative asset URLs remain the most portable build setting. Known root-relative HTML/CSS and dynamic asset URLs now map to the launch's manifest-verified assets. Native root-relative ES imports and worker-body paths still need relative build configuration and are reported by `project_validate`. Precompressed JavaScript/WASM/data `.gz` and `.br` payloads get browser content-encoding headers. Compressed HTML is not a runnable entry point. Admin → Site settings controls upload MiB, expanded MiB, per-file MiB, file count, archive entries and decompression ratio. ZIP64 is supported within those limits. No 100 MiB application ceiling remains, but memory, disk and upstream proxies still determine actual capacity. Accepted bundles are validated without executing submitted code. Multipart spooling and asset delivery avoid holding compressed uploads and binary responses entirely in RAM; the expanded bundle and HTML processing still consume memory. ### Three.js compatibility Prebuilt Three.js/WebGL apps run in the browser player. Bundle Three.js and its loaders locally, use relative asset URLs, and include models, textures, shaders and decoder assets needed by the build. Bare imports such as `import ... from 'three'` require a build step or a working import map. Current Three.js `WebGLRenderer` requires WebGL 2; browser and GPU support remain device requirements. See the [renderer documentation](https://threejs.org/docs/pages/WebGLRenderer.html). Actual acceptance tests cover a 2,000-cube WebGL scene resizing on desktop and three phone layouts, plus a compiled Vite build loading a GLB, a PNG texture and streaming WASM. Real OrbitControls receives trusted mouse and touch gestures, including while outer zoom/position is locked. Native pointer capture, relative movement, Escape release and XR permission delegation are exercised. These checks use Chromium software graphics and emulated viewport sizes; they are rendering/input/layout checks, not hardware benchmarks or confirmation on every physical device. Static assets now include FBX, STL, PLY, Draco, Collada, additional CAD/animation/point-cloud model formats and TGA/TIFF/PVR textures alongside GLTF/GLB and KTX/KTX2. They remain opaque assets, subject to the same path, account-storage and archive limits. Acceptance runs load a real STL, decode a Draco triangle through its WASM worker, and transcode a KTX2 texture through its Basis WASM worker. Pack decoder directories with the build; directory-valued local import-map and decoder paths are retained by the smart importer. Remote assets or backends still need approved origins and compatible browser headers. Three.js WebGPURenderer has executed with an actual WebGPU device on Chromium's SwiftShader/Vulkan test adapter, and separate runs cover its WebGL 2 fallback. An exposed GPU API without an available adapter correctly uses the fallback. Browser/device support, materials and compute requirements remain the application's responsibility; see the [WebGPU renderer guide](https://threejs.org/manual/pages/webgpurenderer). Pointer lock starts from a real player action and Escape releases it without closing the game. WebXR delegates spatial tracking on the isolated origin; immersive VR/AR still requires HTTPS, browser consent and compatible hardware. No physical headset or AR device session is claimed by the automated tests. ### Publishing checker capacity Browser checks and automatic media capture receive an asset budget derived by the server from the selected immutable version, including runtime HTML/CSS transformations, SDK injection and the checked viewports. Submitted check/capture arguments cannot raise that budget. Requests still use only the current runtime capability, without account cookies or Authorization headers, with redirects and external destinations blocked. The checker has separate operator resource ceilings: `CHECK_MAX_ASSET_BYTES` defaults to `33554432` (32 MiB per response), and `CHECK_MAX_TOTAL_BYTES` defaults to `134217728` (128 MiB fetched per operation). Both must be positive safe integers, with the asset ceiling no larger than the total ceiling. Configure these environment variables on the `publishing-checker` service and increase its memory and `/tmp` capacity together when checking larger builds. Raising upload limits or disabling upload size limits does not automatically reserve checker resources. A capacity failure explains these settings; it does not revoke an accepted upload or prevent its ordinary runtime delivery. Checks have a bounded 25–60-second budget based on their advertised-device matrix; capture uses a server-derived 30–240-second budget from validated dimensions, frame count and actions, plus ten seconds for private HTTP cleanup and delivery. Request/concurrency limits still apply. A `checker_deadline` signal means the check is unavailable and can be retried after investigating startup or worker load. Capture defaults to 30 seconds and accepts up to 60 seconds at 15 frames per second. At most 60 declarative pre-capture/timed actions operate the app's real controls; timed actions cannot exceed 60000 milliseconds or the chosen clip length. Short diagnostic clips and thumbnail-only capture remain available. Encoded capture video is limited to 12 MiB, the complete media batch to 80 MiB and its private response to 112 MiB. The checker streams frames into a bounded encoder and cannot execute arbitrary capture JavaScript or reach external backends. Clients should allow up to 300 seconds for capture and verify decoded video scenes before describing the demo. The encoded rate can repeat frames on software rendering; it does not promise 15 fresh samples per second. Five freshly captured screenshots are the default; screenshot_at_ms selects five to eight sorted, unique scene times from 1 through the recording duration in milliseconds. Allowlisted key_down/key_up actions hold and release supported gameplay keys, with cleanup releasing any held keys. Capture replaces draft screenshots by default while preserving the live/history gallery. Initial complete MCP publication requires a validated 30–60-second demo and five screenshots with distinct trusted stored content checksums. The author must still inspect different actual features or scenes; checksums cannot establish that meaning. Source or app-details updates keep existing thumbnail, screenshots and demo unless their refresh is explicitly requested. Retained media keeps its original capture version/fingerprint. Default media_policy: preserve accepts actual unchanged published assets, with explicit legacy count/duration exemptions, while current source/browser checks remain fresh. Missing media is never retained or ready, and legacy exemptions do not satisfy current complete-release requirements. Initial and new/replaced media use current requirements. Optional require_current checks capture provenance and never automatically recaptures. Explicit media_kinds selects only thumbnail, screenshot and/or demo for attachment; unselected media remains intact. Existing-media changes require the separately selected media:write MCP scope. All replaced/detached assets remain recoverable in owner history, including unpublished assets, until explicitly deleted. Upstream checker responses are written incrementally to private temporary files and launch metadata is read from a bounded prefix. Files are removed after delivery or operation cleanup. Playwright reads each delivery file into a browser-protocol buffer, and Chromium must allocate the app's own assets, so browser checking is not constant-memory streaming and cannot guarantee successful execution for every build that fits an account's storage quota. ## Browser acceptance The later MCP/subscription upgrade (operator repository reference: `MCP_AND_PLANS_ADMIN.md`) adds Pro-gated standard clients, resumable binary imports, build-folder publishing, bounded file reads, editable plans and reviewed capability requests. Read [SAVE_SDK.md](/docs/save-sdk.md) before integrating account saves. A compiled Vite/Three build has separately passed WebGL, GLB, WASM, dynamic root assets, native SRI and revocation checks. Testing used the original [2048 source](https://github.com/gabrielecirulli/2048) at commit `478b6ec346e3787f589e4af751378d06ded4cbbc`, with game logic unchanged and analytics removed; its MIT license was retained. A scene built with official [Three.js](https://github.com/mrdoob/three.js) 0.186.1 rendered 2,000 cubes. Neither test establishes a universal graphics performance target. The separate pinned Three r186 graphics fixture decodes actual Draco geometry, KTX2/Basis textures and STL, renders changing pixels with a real software WebGPU device and WebGL2 fallback, and runs an animated shader on an OffscreenCanvas in a native module worker. It checks portrait/landscape drawing-buffer resizing, cross-origin isolation, shared buffers and the administrator's worker denial. Native OrbitControls mouse/touch input, pointer capture/Escape and XR permission delegation are covered separately. Physical GPU performance and immersive headset sessions are not established by these software-rendered tests. See the graphics fixture instructions (operator repository reference: `../tests/fixtures/three-graphics/README.md`). The authored `demo_apps/compatibility-probe` exercises native storage, IndexedDB, workers, service workers, shared WASM memory, embedded pages and app-cookie compatibility. The browser regression adds a disposable local API/WebSocket/frame/form service, confirms approved connections, and denies control/unapproved endpoints. A fresh authenticated browser context resumes 2048 from account data. Third-party test bundles stay in ignored `.local/real-apps/`; no external app code is installed on the server. The retained sample apps are available in your local gallery. Layout regressions use long titles, tags, descriptions and names over 96 page/viewport combinations. See the current verification report for commands and remaining deployment checks. To prepare the pinned fixtures, run `python -m scripts.prepare_compatibility_apps`. Set `HACKEMIST_COMPAT_FIXTURES` to the absolute `.local/real-apps` directory, the local/staging administrator credentials in `HACKEMIST_ADMIN_EMAIL` and `HACKEMIST_ADMIN_PASSWORD`, and `HACKEMIST_BASE_URL`, then run `node --test tests/browser_compatibility.spec.mjs`. It creates retained local/staging sample projects; use a disposable environment. `HACKEMIST_BROWSER_PATH` can select an installed Chrome. The test's external backend is a temporary loopback fixture, so this full transport test is intended for the local Docker deployment.