# Creator guide Build an app or game in your own editor, export its browser files, and upload them through Hackemist Studio. You do not need an AI connection or MCP to publish manually. Hackemist serves HTML, CSS, JavaScript, WebAssembly and assets; build tools, databases and backend servers run elsewhere. ## Prepare your build Export a finished web build from your tool or engine. For React, Vue or another framework, run its production build on your computer and choose the generated dist, build or out folder. For Godot, Unity or another game engine, use its web export. A source project, repository link or folder containing package.json alone is not a playable build. Your selected browser build needs a UTF-8 index.html and every file it loads. A simple bundle looks like this: ```text index.html assets/app.js assets/style.css assets/player.png assets/music.ogg ``` Keep asset references relative to the file that uses them, with the same spelling and capitalization as the uploaded paths: ```html Player character ``` In assets/style.css, use url('./player.png') for that image. For assets loaded from a JavaScript module, new URL('./player.png', import.meta.url) resolves beside that module. Configure your build tool's base path for a relative static deployment. Use a hash-based router or test your router's actual URLs in the player; do not depend on a development server rewriting every route to index.html. Prefer a clean export folder. The importer can detect one browser build inside a larger folder or ZIP and exclude common development files. If it finds several runnable roots, choose just the app you want. Review its included files, excluded files and warnings after import, especially assets loaded through dynamically constructed paths. Keep .env files, private API keys, database passwords, private keys and server credentials out of the upload. Public browser files are visible to players. Uploading Node, Python, PHP or another server's source does not start that server. ## Upload and preview - Open Your apps, choose Create an app, and enter the name, description and category. The app starts as a draft. - Choose Upload HTML or ZIP for a single HTML file or a ZIP bundle. A single HTML file must contain everything it needs or use separately approved external resources. - Choose Import a build folder for index.html and multiple local assets, or Paste HTML for one complete HTML document. - Open Studio's Files & preview tab. Check the import summary and file list, save any edits, then choose Run draft. Studio shows the current Upload limits: upload size, expanded bundle size, per-file size, file count, archive entries and ZIP expansion ratio. Your account's remaining storage also applies. These limits can change with site settings and account access, so use the values shown for your app rather than assuming a fixed allowance. Use a normal, unencrypted ZIP with files and folders; symlinks, unsafe paths and unsupported file types are rejected. Open every meaningful screen in the preview. Fix missing images, fonts, models, decoder files and scripts before continuing. For an existing app, Upload HTML or ZIP, Import build folder and Paste HTML replace the draft's files and create a version; they do not publish it. ## Fit phones tablets and desktops Decide whether your app is a scrolling document, a game that fits the viewport, or a fixed-size stage. Add this to the HTML head: ```html ``` For forms, dashboards and articles, use flexible widths, grid or flex layouts and sensible breakpoints. Declare data-hackemist-layout="responsive" on the body and keep normal vertical scrolling. Let wide screens use their width; reflow sidebars and navigation on narrow screens. For a game, fit the complete stage, HUD, menus and required controls in both dimensions. This starting layout reserves space for the HUD and actions and gives the canvas the remainder: ```html
Score: 0
``` Connect the button and your game logic yourself. A layout declaration tells the player what your app implements; it does not make a fixed scene responsive. Reflow the HUD and menus for portrait and landscape instead of hiding required actions or stretching the scene. Size a canvas's drawing buffer separately from its CSS size. This 2D example caps device pixel ratio at 2 to bound rendering cost; replace draw with your rendering code: ```js const scene = document.querySelector('#scene'); const canvas = document.querySelector('#game'); const context = canvas.getContext('2d'); function draw(width, height) { context.fillStyle = '#15243b'; context.fillRect(0, 0, width, height); } function resizeGame() { const {width, height} = scene.getBoundingClientRect(); if (width <= 0 || height <= 0) return; const dpr = Math.min(window.devicePixelRatio || 1, 2); canvas.width = Math.max(1, Math.round(width * dpr)); canvas.height = Math.max(1, Math.round(height * dpr)); context.setTransform(dpr, 0, 0, dpr, 0, 0); draw(width, height); } new ResizeObserver(resizeGame).observe(scene); window.addEventListener('resize', resizeGame); window.addEventListener('hackemist:resize', resizeGame); resizeGame(); ``` The hackemist:resize event gives CSS width and height, available_width and available_height in the player, and uniform display scale. Lay out the app using its CSS viewport. Keep pointer coordinates in the same coordinate system as your game: use getBoundingClientRect() and scale pointer offsets into your scene's logical width and height. An engine that implements separate logical layouts may declare data-hackemist-layout="adaptive" and data-hackemist-resolutions='[{"width":1280,"height":720},{"width":720,"height":1280}]'. Only advertise layouts you implement. Supported profiles are at most eight unique integer pairs, each dimension 120–8192, with area no greater than 33,554,432 CSS pixels. The player selects and uniformly contains a supported profile; it does not redesign it. For an older fixed game, put the stage and required HUD in one bounded root, mark it data-hackemist-game-stage, and declare data-hackemist-layout="fixed". The player's Fit mode can scale it uniformly. Scaling cannot make tiny text readable, reflow a menu, repair clipped controls or add touch gameplay. Keep the canvas resolution under your engine's control. Test a narrow 320-pixel phone, portrait and landscape phones and tablets, and a wide desktop. Test the menu, active game, pause, settings and results screens, including after rotation and fullscreen changes. The advertised device targets guide the publishing browser checks; a successful startup check does not test every screen. ## Touch keyboard and sound Make the app usable with its native touch or pointer controls. Pointer events cover mouse, pen and touch. Track each pointerId for multi-touch, use pointer capture where useful, and release held actions on pointerup, pointercancel, lostpointercapture and window blur. Apply touch-action: none only to a game surface that needs exclusive gestures. Keep forms and scrolling panels usable. Studio → App details → Player controls offers optional platform buttons and a virtual keyboard. Choose a preset, change labels and map each button to a key your game already handles, then Save player controls and reopen the app. For example, map a Jump button to Space only if your app handles keydown and keyup for event.code === 'Space'. A label does not implement the action. The platform sends untrusted keyboard events and a hackemist:input custom event. Existing standard keyboard handlers often work directly. For a direct integration, button event detail contains key, code, pressed, repeat and source: 'touch'; analog joystick detail contains type: 'joystick', controlId and axes: {x, y}, from -1 to 1. Choose one button input path so you do not handle both the synthetic key and custom event twice. Joysticks return to zero when released. Engines requiring trusted events, physical gamepads or pointer lock need their own adapter or native controls. Players can turn Game and Keyboard on independently, move and resize the overlay, and customize mappings for their device. Keep your own touch controls usable when the overlay is off. Tap an editable field inside the app to use the phone's normal keyboard. The optional virtual Keyboard is a fallback; it cannot grant privileged browser input or replace a game's input handling. The player sound button mutes supported audio/video elements and Web Audio output in the main app document. Your code still starts playback from a real user gesture when the browser requires it. Listen for hackemist:audiochange and read event.detail.muted to reflect the player's choice. Embedded third-party frames and unsupported audio APIs may need your own mute handling. Test muted startup, unmute, pause and return to the app. ## Save player progress Hackemist injects window.Hackemist before your scripts in the player. You do not need to download the SDK or include account credentials. Choose saving according to where your app's state lives: - If the app already reads and writes localStorage, use Studio → Cloud saves to enable automatic localStorage sync, then reopen it. This requires available isolated runtime hosting. Supported localStorage and app cookies can sync up to 5 MiB; memory variables, sessionStorage, IndexedDB, external accounts and third-party frames are not captured. - If progress lives in JavaScript objects or an engine, integrate Hackemist.createProgressStore with capture, validated restore and initial-state callbacks. Include every durable field needed to resume: level, score, seed, inventory, board and quest state as appropriate. A switch or a save-schema declaration cannot discover them. This SDK starter belongs inside an async startup function or module script. Adapt state and render to your app, and implement the named input, status, error and recovery UI functions before using it: ```js let state = {level: 1, score: 0}; const saves = Hackemist.createProgressStore({ capture: () => ({...state}), initial: () => { state = {level: 1, score: 0}; render(); }, restore: saved => { if (!saved || !Number.isInteger(saved.level) || saved.level < 1 || !Number.isFinite(saved.score) || saved.score < 0) { throw new Error('Unsupported saved progress.'); } state = {level: saved.level, score: saved.score}; render(); }, onStatus: status => showSaveStatus(status), onError: error => showSaveError(error.message), onRecovery: details => { showRecoveryChoices(details); return 'stop'; } }); try { await saves.ready; enableGameplay(); } catch (error) { showSaveError(error.message); // Keep recovery choices visible. } // After a meaningful change: // state.score += 10; saves.changed(); // At a checkpoint: const result = await saves.flush(); // Only result.saved === true confirms a newly acknowledged save. // After the player chooses a recovery option: // await saves.resolve('local'); // Or 'cloud' / 'fresh', then enable gameplay. ``` Capture finite plain JSON within 64 KiB, without functions, DOM nodes, GPU resources, cycles or arbitrary class instances. Restore completes before play begins. Rebuild the scene from the data rather than saving a renderer or entire engine object. The helper polls, debounces and serializes changes; saves.changed() speeds up detection, and await saves.flush() lets you wait at a checkpoint. Only status 'saved' confirms the latest snapshot was acknowledged. Offline play, a crash or closing before acknowledgement can lose unsaved changes. A different local and cloud save, invalid cache or unbound older cache pauses writes for the player's choice. An empty cloud slot must not silently erase local progress. Keep the recovery interface visible, preserve originals, and call saves.resolve('local'), saves.resolve('cloud') or saves.resolve('fresh') only after the player chooses an allowed option. Replace independent lifecycle cache writes with saves.cacheLocal(); that method means browser recovery, not a cloud save. A failed read or write pauses saving; deliberate saves.reload() restores the current cloud save before resuming. Guests have no account saves. Test while signed in through the player, then refresh, close and reopen, use another device, switch accounts, and try a named save slot. Switching slots requires reopening the app. Test offline and conflicting tabs without discarding the earlier progress. The Cloud saves tab's source hints help inspect integration but cannot prove complete save coverage. An optional hackemist-save.json describes the save format; preserve compatibility or implement a migration when changing it. The [save SDK guide](/docs/save-sdk.md) covers recovery and migrations. Start with the [complete runnable progress counter](/docs/progress-example.md); it includes all status, checkpoint and recovery UI functions. ## Add multiplayer or an external backend For built-in shared rooms, open App details → Browser compatibility → Multiplayer & connections. Enable shared multiplayer rooms, presence, and messaging, choose Save multiplayer option, and reopen the app. The administrator must have enabled the room service and creator runtime options. Your source still implements the shared game, quiz, chat or collaboration rules. Use these SDK calls inside your app's async multiplayer startup, with your own renderSharedState and handlePlayerEvent functions: ```js const client = await Hackemist.multiplayer.connect(); client.on('state', snapshot => renderSharedState(snapshot.state)); client.on('broadcast', event => handlePlayerEvent(event)); const invitation = await Hackemist.multiplayer.invitation(); const room = invitation ? await client.joinRoom(invitation) : await client.createRoom({maxPeers: 8}); // Show room.code or room.invite_url in your invitation UI. await client.send('chat', {text: 'Hello!'}); // Only the current host may change shared state, using its known revision: // await client.setState({round: 1}, client.revision); // When leaving: await client.leaveRoom(); client.close(); ``` Validate every message and game action, render chat as text, and handle presence, reconnects and stale revisions. Rooms are separated by app version and preview/live mode. Room state is temporary and can disappear on expiry or service restart; save durable results separately. A host-controlled room is not an independent anti-cheat server. The [room SDK guide](/docs/multiplayer.md) shows the complete room flow. For your own API, database gateway or authoritative game server, deploy the backend separately. In Browser compatibility, enter each exact HTTPS or WSS connection origin, without a path, and request review with a reason. For example, request https://api.example.com and wss://rooms.example.com. External assets, frames and form destinations have separate origin settings. Approval allows the connection; it does not create a backend, add networking code, grant database access or authenticate the app's users. Configure backend CORS for the actual isolated runtime origins, which can differ by account, app and save slot. Use the backend's own browser authentication and server-side authorization. Keep privileged provider keys and database credentials on that backend; use scoped, short-lived browser authorization when needed. Hackemist account cookies and MCP tokens are not exposed to uploaded apps. Test failures, timeouts, reconnects and your service's quotas in the actual player. ## Three.js workers and WASM Bundle Three.js, its addons, models, textures, decoder files and fonts in the browser build. A Vite or React Three Fiber source project needs its production build first. GLB/GLTF and many other model formats are accepted, within the same upload limits. Point Draco and KTX2/Basis loaders to your packaged decoder directories; include their JavaScript and WASM files. Call KTX2Loader.detectSupport(renderer) for texture support. For an existing perspective camera and renderer, resize both the drawing buffer and camera to the real container: ```js const sceneHost = document.querySelector('#scene'); function resizeScene() { const {width, height} = sceneHost.getBoundingClientRect(); if (width <= 0 || height <= 0) return; renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); renderer.setSize(Math.floor(width), Math.floor(height), false); camera.aspect = width / height; camera.updateProjectionMatrix(); } new ResizeObserver(resizeScene).observe(sceneHost); window.addEventListener('hackemist:resize', resizeScene); resizeScene(); ``` Give the container a real height and the canvas width: 100%, height: 100% and display: block. Reflow HTML HUDs independently. Use native touch gestures or authored movement/look controls on phones. Request pointer lock from a real user click; Escape releases it. Overlay keys cannot provide raw mouse movement or a privileged user gesture. Mouse capture, Web Workers, WASM threads and VR/AR follow the administrator's site-wide graphics settings. Check their availability in Browser compatibility. Workers, WASM threads and VR/AR need configured isolated runtime hosting; WASM threads also need workers and compatible cross-origin isolation. External resources need appropriate CORS/COEP headers. Platform permission cannot add browser, GPU or device support. Test the exported build in the player, provide a normal-screen or lower-cost fallback, and state real requirements. Managed service-worker caching and embedded frames retain separate approval. The [runnable graphics example](/docs/graphics-example.md) provides GPU detection, a bounded quality setting and a readable 2D fallback; the [app manifest guide](/docs/app-support.md) describes optional requirements. ## Complete the listing and media In App details, set an accurate name, description, category, subcategory and useful tags. In Listing & gallery, select supported controls, device targets, orientation, languages and accessibility features. Add engine/framework, requirements, licence, credits and release notes where relevant. Advertise only what you have tested. If the administrator has granted your account Plus publishing, App details offers Plus visibility even while the wider creator program is paused. Players need account access whose plan includes Plus apps; your publishing grant does not assign a viewer plan or enable earnings. Without a grant, Plus is unavailable while the program is paused and stored Plus apps are temporarily free. If the administrator revokes the grant, the stored choices remain and the apps become free during the pause. Enabling the wider program restores the existing eligibility rules. Monetization and creator financial actions remain paused until the program is enabled and their separate requirements are met. Use a clear thumbnail, five distinct screenshots of real functions or scenes, and a 30–60-second demo as a practical target for a complete listing. Show a menu or starting point, representative interaction and a readable result. Do not submit five copies of the same scene. Add captions, alt text and descriptions that match what the images and recording show. Manual Studio publication uses the site's current gate; five screenshots and a 30–60-second demo are required by the complete MCP release workflow, not every manual upload. The listing thumbnail frame is 16:10. A 1280 × 800 image fits it directly; other shapes are fitted uniformly so the whole image remains visible. Keep text readable at card size. Use up to eight screenshots to show actual desktop, phone and tablet layouts where supported, for example three desktop, three phone and two tablet views. Capture the app at those sizes; do not stretch a desktop screenshot to imitate a phone. Each view should show a useful feature or state, with an honest device and scene description. An authorized MCP agent can request native screenshots with `screenshot_plan`, specifying each viewport, time and scene. The default captures eight views across the device families advertised by the app, and uses a separate native 1280 × 800 thumbnail. The demo retains its selected video dimensions. Inspect every image and the real recording before publication; changing a viewport or adding a scene label does not prove the interaction worked. Existing media is retained during updates unless you request a refresh; replacing it through MCP requires separate media consent. See [the capture workflow](/docs/autonomous.md#capture-the-app-itself). Studio accepts up to eight static PNG, JPEG or WebP screenshots, each up to 8 MiB. A thumbnail can also be GIF, up to 8 MiB. Demo uploads are MP4/H.264, up to 60 seconds and 16 MiB, with dimensions no larger than 1920 on either side and area no larger than 1920 × 1080. Audio is removed from the published demo. Review the processed media before publishing. You can reorder screenshots, edit media text, replace a thumbnail or demo, and remove media from the draft. Replaced and removed files remain in Media history, including unpublished media, and can be restored to the draft. Retained files count toward storage and the 40-file project media limit. Permanent deletion is a separate confirmed action; files selected in the draft or current live release are protected. ## Check and publish - Save your source, App details and Listing & gallery changes. - Run the draft and test real interactions, direct touch, keyboard, audio and every advertised orientation. Verify saving and multiplayer through signed-in player sessions where applicable. - Open Publishing checks, resolve blocked findings, and Run browser check for the saved build. Fix console errors, missing assets, blocked requests and clipped required controls. Check the reported media inventory and account requirements. - Review the app page, gallery and release notes, then choose Publish or Publish changes when the draft is ready. Browser checks exercise bundled startup and layout at advertised viewports. They do not verify gameplay, authenticated save/restore, external APIs, multiplayer services or real device performance. The active site settings determine which checks block manual publication. A ready checklist is a preflight result; publication is a separate action. Use assets and code you have permission to distribute, include licence credits, and explain external data collection. Keep personal data and credentials out of public files and screenshots. Provide a support and deletion path for player data held by your own backend. ## Share a private app Choose Private in App details, save and publish when the app is ready. A private app stays out of public discovery, Launches, creator profiles and search-engine listings. Copying its `/app/...` link does not grant another account access. The owner and administrators retain their existing access. Open **Private access** in Studio, enter the person's registered username and optionally choose an expiry. Share the app link with that person; they must sign in to the account you granted. You can prepare grants before publication, but they only open the current published private release. Grants remain saved across visibility changes and apply again when the app is private. Revoke a grant in the same dialog when access should end; expiry and revocation are enforced by the server. A signed-in person opening a shared app link without access can choose **Request access** and add an optional message of up to 500 characters. Sending a request does not open the app. The creator reviews **Access requests** in Studio's **Private access**, then chooses **Approve** with an optional expiry or **Deny**. Approval lets that account open the published private release. Where notifications are available, targeted in-app notices link the creator to Studio and tell the requester the decision. These notices do not send email or notify followers. Repeated submissions do not create extra requests, including after a decision. The creator can still grant a registered username directly after denying a request. Approved access uses the same account grant and can later expire or be revoked. Private access permits playing the published app and viewing its published listing. It does not grant Studio access, draft previews, build downloads or analytics. New drafts remain private to their owner, and normal account play requirements still apply. For authorized MCP clients, see [the private access tools](/docs/mcp.md#private-account-access). ## Update an existing app Open the same app in Studio, import a new browser build or edit its files, and save. Source saves create immutable versions. App details, listing and media edits stage a numbered draft when needed and reuse it for further metadata edits; an unchanged metadata save creates no new version. The published build and listing stay visible while you prepare the update. Existing media remains selected unless you change it. Preview and check the saved draft, then Publish changes to release its build, details and gallery together. A details-only release also gets a version number. If a tab reports an edit conflict, preserve your typed work and reload the latest draft before retrying. Version history can restore an older build to the draft; publish separately to make it live. Download build gives you a copy to keep outside Hackemist. Making an app private restricts access immediately. Making it public again takes effect when you publish. Keep compatible saves working across releases; a new app build does not replace a player's saved progress by itself. For help, use Contact and rights requests or email support@hackemist.com. # App names and public links Studio → App details includes an **App URL slug** with a live link preview. It fills from the app name until you edit it. Choose lowercase letters, numbers and hyphens, for example `neon-district`. Save your details draft and publish to use `https://hackemist.com/app/neon-district`. The server checks availability; another app's current or retained URL cannot be claimed. Published URL changes take effect with publication. Existing saves and permissions stay attached to the same app. MCP creators can send `slug` through `project_update` with the current listing revision; read the returned `app_path` rather than constructing a URL from the internal project ID. Public and Plus listings can appear in search. Private apps stay out of public discovery and sitemaps and require creator permission. Plus previews remain public, while launching requires a plan with Plus access. Guest players may sign in for supported account saves or skip and play without signing in.