Publisher guide
Package & manifest
What goes into the zip, what the manifest declares, how index.html is structured and how assets are resolved.
Package layout
my-game/
vibe-app.json manifest (required)
index.html entry fragment (required)
icon.png PNG, 32x32 to 512x512 (required)
main.js scripts (.js / .mjs)
style.css stylesheets
levels.json data (.json / .txt)
sprites/*.png images (.png .jpg .jpeg .gif .webp .svg)
sounds/*.mp3 audio (.mp3 .ogg .wav .m4a)
videos/*.mp4 video (.mp4 .webm; both also play in <audio>)
fonts/*.woff2 fonts (.woff .woff2 .ttf)Zip the files so that vibe-app.json is at the root of the archive. A single shared top-level folder (as most zip tools create) is accepted and stripped.
File rules and limits
- File names: letters, digits,
.,_and-; folders separated by/; at most 8 levels; no leading dots. index.htmlis the only HTML file..htm,.xhtml,.xmland.svgzare rejected.- No archives, executables or anything outside the extension list above. Binary files must match their extension (magic bytes are checked).
- Text files must be UTF-8. Every
.jsonfile must parse.
| Limit | Default |
|---|---|
| Package size (zip) | 50 MB |
| Uncompressed size | 200 MB |
| Files per package | 2000 |
| Single file | 50 MB |
| Versions per app | 50 |
vibe-app.json
{
"name": "My Game",
"version": "1.0.0",
"description": "One or two sentences for the store.",
"icon": "icon.png",
"orientation": "portrait",
"permissions": [],
"leaderboards": [
{ "id": "high-score", "name": "High score" },
{ "id": "lap-1", "name": "Lap 1", "order": "asc", "format": "time" }
],
"events": [
{ "id": "level-complete", "name": "Level completed" }
]
}| Field | Required | Rules |
|---|---|---|
name | yes | 1 to 60 characters. Shown in the store and on the home screen. |
version | yes | Semantic version such as 1.2.3. Becomes the version label of the upload. |
description | no | Up to 500 characters. |
icon | yes | A PNG inside the package, 32x32 to 512x512 pixels. |
orientation | no | any (default), portrait or landscape. |
permissions | no | Array of strings. The only permission today is pointer-lock. |
leaderboards | no | Up to 10 boards. id: 1 to 32 letters, digits, '.', '_' or '-' (what your code passes to vibe.leaderboards); name: what players see, up to 40 characters; order: desc (higher scores first, default) or asc (lower first, e.g. times); format: number (default) or time (milliseconds, shown as 1:23.456). They only show once you turn leaderboards on in the app's settings. Changing a board's order later keeps the stored bests; give it a new id instead. |
events | no | Up to 20 events your code counts with vibe.analytics.track for your statistics. id: the same rules as for leaderboards; name: what your statistics show, up to 40 characters. Only declared ids are counted. |
index.html
The entry is a fragment, not a document. Its top level may only contain stylesheet links, script tags with a src, and exactly one <div data-vibe-root> that wraps all of your markup.
<link rel="stylesheet" href="style.css">
<div data-vibe-root>
<h1>My Game</h1>
<canvas id="game" width="320" height="480"></canvas>
<a href="https://example.com/help" target="_blank">Help</a>
<button id="exit">Exit</button>
</div>
<script type="module" src="main.mjs"></script>The validator rejects:
<html>,<head>,<body>,<meta>,<base>,<title><iframe>,<object>,<embed>,<form action>and any<link>other than a stylesheet- Inline scripts,
on*attributes,javascript:URLs. Attach handlers from JavaScript. - Any
src,href,srcsetorposterthat does not point to a file inside the package (data:URLs are fine for images). - Links that are not absolute
http(s)URLs withtarget="_blank", except in-page#anchors. The host asks the player before an external link opens.
Assets and paths
Relative paths in HTML attributes, CSS url(), static import specifiers and plain string literals in JavaScript that name a package file are rewritten at publish time to vibe-asset://path tokens. At runtime each token becomes a blob: URL created inside your app. You may also write vibe-asset://sprites.png yourself.
// Static references are rewritten at publish time:
// <img src="sprites/hero.png"> CSS url("sprites/bg.png") import { ai } from './ai.mjs'
// fetch('levels.json') new Audio('sounds/hit.mp3')
// all become vibe-asset://... tokens and blob: URLs at runtime.
// Paths computed at runtime go through vibe.asset():
const img = new Image();
img.crossOrigin = 'anonymous'; // needed before drawing on a readable canvas
img.src = vibe.asset(`levels/${level}.png`);
const data = await fetch(vibe.asset('levels.json')).then((r) => r.json());- CSS: no
@import, no externalurl(), noexpression()orbehavior:. - SVG files: no scripts, event handlers,
<foreignObject>,<a>or external references. - A reference to a file that is not in the package is an error. Files that nothing references produce a warning and are still shipped.
Scripts and modules
Scripts are listed in index.html and run in order after the root div exists. Use type="module" for ES modules; files ending in .mjs are always treated as modules.
// main.mjs
import { winner } from './rules.mjs'; // relative imports only, no bare specifiers
const ctx = await vibe.ready; // top-level await is fine in modules
// rules.mjs
export function winner(board) { /* ... */ }- Import package files with a relative path such as
./rules.mjs. Bare specifiers and URLs are rejected. - Circular imports are rejected. Bundle the code into one file instead.
- Dynamic
import()works for package files; a computed specifier is a warning because only package files can be loaded at runtime anyway. - The language level is ES2026 (latest); there is no transpiling, so the player's browser must support what you use.
Opaque-origin gotchas
Your app runs in an opaque origin: it has no origin of its own, which is what makes the sandbox tight. A few browser behaviours follow from that.
- Images are cross-origin. Set
img.crossOrigin = 'anonymous'before drawing an image on a canvas you want to read back (getImageData,toBlob). window.onerroronly sees"Script error."for some errors. Wrap risky code intry/catchwhile developing.localStorage,indexedDB, cookies and the Cache API throw. Usevibe.local,vibe.dbandvibe.cloud.- Nothing can be fetched from the network, including from the VibeGames servers. Ship every file in the package.