VOPE · Hosted Apps
VOPE itself doesn't have AI built in. There's no chatbot, no auto-generated content, and nothing reading your messages. But that's not the whole story: bring your own AI assistant, whether that's ChatGPT, Claude, or anything else you like, and build something small and fun together to share right here in VOPE with your friends or family. This page is what makes that possible, and it's also the complete technical spec if you're the one writing the code by hand.
๐ Start here, no coding required
Copy the prompt below into ChatGPT, Claude, or any AI chatbot that can read a web page. It tells the AI everything it needs, including the rules further down this page, to build a small trivia game with a shared, live leaderboard your whole group can play.
I want you to build a small trivia game as a "VOPE Hosted App" for my friend
group. First, read the full specification at https://vope.me/apps/spec.html and
follow every rule on it exactly, especially the constraints in ยง1 and the
viewport rules in ยง2. (If you can't open that link yourself, tell me and I'll
paste the page's text in for you.)
The game should:
- Show one multiple-choice question at a time, with 4 answer options.
- Keep score for whoever's playing, using window.vope.data.private.
- Keep a shared leaderboard visible to the whole group, using window.vope.data.shared.
- Reveal the right answer after each guess, then move to the next question.
- Include at least 10 questions about a topic your group would enjoy: movies,
your hometown, inside jokes, anything you like.
Make it colorful and fun. It must be a single, self-contained HTML file with
no external images, fonts, or scripts, and no network access of any kind.
The spec explains exactly why.
.html file.or, skip building entirely
Grab a ready-made app from the App Library and share it as is, or copy one and ask an AI to change it into something else.
๐ง The full spec, for developers and for whichever AI is doing the building
A VOPE group can share small web apps directly in a chat: a poll, a simple game, a shared scoreboard, a tool. Each one is a single HTML file, rendered inside a locked-down viewer on every member's phone. It is not a native mobile app. It has no listing of its own on any app store, is never separately installed, and cannot be distributed outside this one feature. Full technical and policy background: App Store & Google Play compliance.
An app has no way to know, at the time it's written, whether it will end up served peer-to-peer from a group member's phone or from VOPE's own servers. That choice is made later, by whoever shares it, not by you, so write against the stricter case always. "It'll be fine once it's on VOPE's servers" is not a valid escape hatch for any rule below.
No loading images from a web address, no calling any outside service. Everything the app needs has to be built in.
Just one .html file: no separate style sheets, scripts, or a build step of any kind.
Small amounts of data only: think a score or a short list, not photos or long histories.
If it's hosted from someone's phone, it can disappear the moment that phone does. That's normal, not broken.
No "buy" button, no wallet, nothing money related. There's no way to process one anyway.
Don't write code that tries to find a way around these rules, even out of curiosity.
There's no console for whoever's testing this, so if something breaks, put the reason on screen instead of failing silently.
| Constraint | What it means for the code you write |
|---|---|
| SHOULD surface its own errors on screen | A Hosted App runs with no browser devtools available to whoever's testing it. If a
script throws during setup, the only visible symptom is "nothing happened," which is
indistinguishable from a slow network to a non-developer. Wrap the app's startup code
in a try/catch (or a window.onerror handler) that renders a
short, plain-language message into the page itself on failure, rather than leaving a
blank or frozen screen. This is a strong recommendation, not an enforced constraint.
Nothing blocks a Hosted App that skips it, but it's the difference between a bug
report you can act on and "it doesn't work" with no further information. |
| MUST NOT use the network, ever | No fetch, XMLHttpRequest, WebSocket,
<img src="https://โฆ">, CDN scripts, web fonts, or API calls of any
kind. This is enforced by a Content Security Policy the app cannot see, override, or
detect, so it will simply fail silently. Every asset, whether a font, image, or icon,
must be inlined as a data URI or omitted. |
| MUST be a single HTML file | No build step, no bundler, no separate .js/.css files to
reference. Inline every <script> and <style>
directly into the one file you hand back. |
| MUST respect the storage caps | โค100KB per key, โค5MB total per app, across both shared and
private storage combined (see ยง2). Design data structures assuming this
ceiling from the start, rather than building something that needs unbounded storage. |
| MUST NOT assume it's always reachable | A phone-hosted app can go offline the instant its host's phone does. Build for that being routine, not exceptional, by showing a reasonable state when a peer disconnects. |
| MUST NOT request or process payments | No payment form, no crypto wallet integration, no "buy" button of any kind. There is no network path to a payment processor regardless, but don't build the UI for one either. |
| MUST NOT attempt to detect, escape, or disable the sandbox | No code whose purpose is probing for a way around the CSP, the storage scope, or the navigation lock. This is a content-policy violation independent of whether it would technically succeed. |
A recurring failure in AI-generated Hosted Apps, games especially, is a layout that assumes a full mobile browser window, complete with the browser's own address bar and often a phantom bottom bar "just in case." Its real controls then end up clipped, overlapped, or scrolled out of view. Neither of those exists here. Get this wrong and a game's bottom row of buttons, or a top scoreboard, can render partly or fully outside what the person can actually see or tap.
No browser address bar, no hidden toolbar eating space. Your space is your whole space, so if something's clipped, it's your layout, not VOPE's.
VOPE's own tab strip, and often an ad banner, sit outside your app and take real space with them. Design for a shorter screen than you'd guess.
Don't design for one specific phone. Use percentages and flexible layouts so it looks right on any device.
Build with flexbox or grid so header, board, and controls share the real height, instead of pixel positions guessed in advance.
Lay out with CSS that adjusts automatically, rather than measuring the screen once and never again.
Not every app fits in one fold at every size. A scrollable page, or a menu with jump-to-section buttons, beats content silently cut off below the edge.
| Fact | What it means for your layout |
|---|---|
| Your app is shown inside a chat screen, below VOPE's own header and a row of app/chat tabs. It is not full-screen, and not a browser tab | The space you're given is already exactly your document's viewport before your CSS runs. VOPE does not overlay anything on top of or inside your rendered area at any time: no toolbar, no button row, no bottom nav. If something looks clipped, it is your own layout, not VOPE's chrome eating space you assumed you had. |
| There is no browser address bar, tab strip, or home-indicator gesture area to leave room for | Don't reserve top or bottom padding "to be safe" against browser chrome, because there isn't any. A game built to dodge a phone browser's collapsing address bar will instead leave an unexplained gap or push real content off screen here. |
| The available width and height varies by device and is never a fixed size or aspect ratio | Never hardcode pixel dimensions or design for one specific phone's screen. Lay out with
percentages/vw/vh/flexbox/grid so the same file works correctly
on a small phone and a large one. |
| MUST size the root layout to fill exactly what's given, and nothing more | Set html, body { margin:0; width:100%; height:100%; } and build your game's
root container with display:flex; flex-direction:column; height:100%; (or
CSS Grid) so header/board/controls share the real available height proportionally,
instead of a fixed-position element computed against an assumed screen size. If your
layout truly needs a firm bottom row, such as a score, D-pad, or action buttons, make it
a normal flex child at the end of the column. Avoid position: fixed; bottom: 0
against 100vh: that's exactly the pattern that goes wrong when the assumed
viewport doesn't match the real one. |
| Orientation and resize: a person can rotate their phone or (rarely) resize the window while your app is open | Don't calculate layout once on load and never again. Use CSS that reflows (flexbox,
grid, percentages) rather than JavaScript that measures window.innerHeight
once and hardcodes pixel values from that single reading. |
| Roughly how much shorter than a full phone screen you should expect: VOPE's tab strip above your app, and an ad banner below it when one is shown (an ad-free subscriber has no bottom banner at all), both sit outside your rendered area rather than overlaid on top of it, but they still claim real height before your app ever sees a pixel. Together they typically take somewhere in the range of a small double-digit percentage of a phone's screen height. The tab strip is a fixed, short row, and a mobile banner ad is usually on the order of 50 to 100 logical pixels tall. Neither figure is contractual: it changes by device, by whether ads are showing at all, and could change in a future VOPE release. | Never design against an assumed "close to 100% of a typical phone" canvas. Budget for
meaningfully less, and verify by actually opening your app inside VOPE, not just in a
desktop browser tab, before calling it done. There are two valid ways to handle a
layout that's taller than what you're actually given. The first, and the preferred
approach for a game specifically, is to compress: since a game's whole
point is usually visible at once, make every element's size and spacing relative,
using %, vh, flex-grow, and clamp(),
so the same layout proportionally shrinks to fit whatever height it's handed, with
nothing dependent on a specific pixel value fitting. The second is to
let it scroll: for content that genuinely can't compress below a
usable size, such as a long form, a detailed settings panel, or a multi-round quiz, a
normal scrollable page is completely fine, as is a short top-level menu whose buttons
jump to sections using element.scrollIntoView() or plain in-page anchor
links, rather than trying to cram everything into one screen at a size nobody can
actually read or tap. What's not fine is a layout that assumes it all
fits, silently doesn't, and leaves no way to reach whatever scrolled out of view.
That's the actual bug this row exists to prevent, found from a real game that needed
scrolling just to reach its own controls. |
window.vope
The bridge is the only way a Hosted App reads or writes anything durable. There is
no localStorage/IndexedDB as a source of truth: the sandbox treats
on-device storage as ephemeral by design (see the compliance page). Every method below
returns a Promise.
window.vope.data.shared: visible to every group member with access to this appawait window.vope.data.shared.set('score', '42');
const score = await window.vope.data.shared.get('score'); // '42'
window.vope.data.private: visible only to the current userawait window.vope.data.private.set('lastGuess', 'blue');
const guess = await window.vope.data.private.get('lastGuess');
window.vope.versionAn integer, currently 1. Check it if your app depends on a bridge capability
introduced after this spec's current version. There are none yet beyond the four methods
above.
delete/list-keys
operation, no vope.share(), no vope.close(), and no way to read
anything about the current user (name, photo, uid). A Hosted App genuinely cannot tell who
is using it beyond what it stores itself via data.private. Do not invent or
call a method not listed here: it does not exist, and the call will simply fail.
Copy this as a starting point. It already respects every constraint above.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Tally</title>
<style>
body { font-family: sans-serif; text-align: center; padding: 40px 16px; }
button { font-size: 24px; padding: 12px 28px; border-radius: 10px; border: none; }
#count { font-size: 48px; margin: 24px 0; }
</style>
</head>
<body>
<h1>Group tally</h1>
<div id="count">—</div>
<button id="inc">+1</button>
<script>
async function render() {
const value = await window.vope.data.shared.get('count');
document.getElementById('count').textContent = value || '0';
}
document.getElementById('inc').addEventListener('click', async () => {
const current = parseInt((await window.vope.data.shared.get('count')) || '0', 10);
await window.vope.data.shared.set('count', String(current + 1));
render();
});
render();
</script>
</body>
</html>
A Hosted App is user content, held to VOPE's normal content rules (see the Terms of Service, ยง10, Hosted Apps, for the exact language). In short: nothing illegal, and no adult or sexual content of any kind, with no exception, not even behind a warning or an age check. That prohibition is separate from, and stricter than, the "mature content" flag whoever shares an app can set in the VOPE app itself for otherwise lawful mature themes like violence. That flag is never a way to host content this section prohibits.
For a coding tool that wants to validate calls programmatically rather than parse this page's prose:
{
"specVersion": 1,
"bridgeVersion": 1,
"methods": {
"data.shared.get": { "args": ["key"], "returns": "string | null", "async": true },
"data.shared.set": { "args": ["key", "value"], "returns": "void", "async": true },
"data.private.get": { "args": ["key"], "returns": "string | null", "async": true },
"data.private.set": { "args": ["key", "value"], "returns": "void", "async": true }
},
"constraints": {
"networkAccess": false,
"singleFile": true,
"maxKeyBytes": 100000,
"maxTotalBytes": 5000000,
"guaranteedReachability": false,
"paymentsAllowed": false
}
}