VibeGames

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

A typical package
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.html is the only HTML file. .htm, .xhtml, .xml and .svgz are 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 .json file must parse.
LimitDefault
Package size (zip)50 MB
Uncompressed size200 MB
Files per package2000
Single file50 MB
Versions per app50

vibe-app.json

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" }
  ]
}
FieldRequiredRules
nameyes1 to 60 characters. Shown in the store and on the home screen.
versionyesSemantic version such as 1.2.3. Becomes the version label of the upload.
descriptionnoUp to 500 characters.
iconyesA PNG inside the package, 32x32 to 512x512 pixels.
orientationnoany (default), portrait or landscape.
permissionsnoArray of strings. The only permission today is pointer-lock.
leaderboardsnoUp 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.
eventsnoUp 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.

index.html
<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, srcset or poster that does not point to a file inside the package (data: URLs are fine for images).
  • Links that are not absolute http(s) URLs with target="_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 vs. computed paths
// 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 external url(), no expression() or behavior:.
  • 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.

ES 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.onerror only sees "Script error." for some errors. Wrap risky code in try/catch while developing.
  • localStorage, indexedDB, cookies and the Cache API throw. Use vibe.local, vibe.db and vibe.cloud.
  • Nothing can be fetched from the network, including from the VibeGames servers. Ship every file in the package.