velven
Docs
Menu

Hosting

Velven hosts your space: publish a folder and, once it passes a safety check, it is live at its own velven.io address, played in the frame on its Velven page with the SDK added. A space already live on one of 7 other hosts can be linked instead, and moved onto Velven's hosting whenever you like.

View as Markdown

Publish on Velven

Publish the folder your build writes, with an index.html at its top. There are 3 ways in, and they make the same versions: the Velven CLI, Upload files on the add page (a folder, a zip, one HTML file or pasted HTML), and the publish tool of Velven's MCP server for an assistant in a chat.

Terminal
npx @velven/cli loginnpx @velven/cli publish ./dist          # a private preview; the link is printednpx @velven/cli publish ./dist --prod   # live once Velven's safety check passes
  • Only files Velven does not already have are uploaded, so publishing again after a small change is quick.
  • A plain publish makes a preview: a private link, nothing public changes.
  • --prod sends the version to the safety check. When it passes, the version replaces the live one and the space is on Velven. The first time, that makes the listing.
  • Each space gets its own address, 8 random letters and digits on velven.io, such as k3xq7vtm.velven.io. It is served only inside the frame on the space's Velven page.

Note: No account yet? A publish without signing in makes an unlisted page you can claim later. See Publish without an account.

velven.json

The listing and the hosting settings come from velven.json in the folder you publish. Velven never guesses them. The CLI asks for a missing required field in a terminal and saves your answer into the file; anywhere else it stops and names each missing field. Flags such as --title override the file for one run. The file is never part of a version: Velven does not upload or serve it, nor a velven.json in any folder below.

velven.json
{  "title": "Star Hop",  "type": "game",  "devices": ["desktop", "mobile"],  "description": "Hop between stars before the light runs out.",  "engine": "three.js",  "start": { "click": "Play" },  "boards": [{ "key": "main", "trust": "client", "display": "both" }],  "stats": [{ "key": "stars", "label": "Stars" }],  "achievements": [{ "key": "first-hop", "title": "First hop", "description": "Reach another star" }]}

Listing

titleUp to 80 characters
Required. The name people see on Velven.
typegame, world, tool or wonder
Required. What kind of space it is.
devicesA list of desktop, mobile, vr
Required. What it works on.
descriptionUp to 160 characters
One line on what the space is.
engineAn engine's value
What it is built with. Allowed values.
ai_toolsA list of tool values
The AI tools it was made with.
modelsA list of model values
The models it was made with.
how_madeUp to 2,000 characters
How it was made, the prompt included if you like.
source_urlA web address
Its source code, if public.

The listing fields become the space's when you publish to production. A preview never changes the public listing.

Hosting

entryA path in the folderDefault index.html
The page the frame opens.
spatrue or falseDefault false
Serve the entry page for any path with no file and no extension, for client-side routing.
sdktrue or falseDefault true
false stops Velven adding the SDK's script tag. Set it when you bundle @velven/sdk yourself.
toaststrue or falseDefault true
false stops Velven drawing its toast for an achievement unlock, when your space draws its own.
start{ "click": "Play" }, { "click": [x, y] } or { "key": "Space" }
How to get past a start screen, for the safety check. A click on the button or link with that text, a click at a point in a 1280 by 720 frame, or a key by its name, such as Enter or ArrowUp.

Leaderboards, achievements and stats

boardsA list of boards
The same shape as the page's boards block. Board fields.
statsA list of stats
Named counters per player. Achievements and stats.
achievementsA list of achievements
An icon can be a file in the folder, by its path. Achievements and stats.

They apply when the version goes live, never on a preview. A version that names boards makes its list the space's boards, and one that names stats or achievements makes those two lists the space's: a key you drop is withdrawn with its scores and unlocks kept. A version that names none of them, such as one published with the MCP server's publish tool, leaves the space's as they are. Rolling back brings the older version's back. A preview's scores and unlocks go to the sandbox against the live space's declarations.

Written by the CLI

spaceA space's slug
Written by the first publish. Later publishes update that space; publishing to a space you do not own is refused.
claimA claim token
Written by a publish without an account. Publishing again updates the same unlisted page; claiming replaces it with space.

A .velvenignore file in the folder leaves files out: one pattern a line, with * and ? as wildcards, a doubled * for any depth of folders, a trailing / for folders and # for comments. Dotfiles, node_modules and every velven.json, in any folder and any case, are always left out, whichever way you publish. The CLI also leaves out a name that ends in a dot or a space or holds : or ~ (one velven dev would not serve), and names each. A link to a file or folder inside the folder is followed, a folder link's files published under the link's name; a link outside the folder, to one of those, or back up to a folder it sits in is left out, and the CLI names it.

Limits

One version
Signed in
100 MB and 2,000 files
Without an account
50 MB and 1,000 files
Publishes
Signed in
30 an hour and 200 a day per account
Without an account
10 an hour and 30 a day per network address
Spaces
Signed in
No limit
Without an account
Each unlisted page is deleted 7 days after its first publish unless claimed
Versions kept
Signed in
The live one and the last 20
Without an account
The live one and the last 20
A preview link
Signed in
24 hours
Without an account
No previews: each publish is checked, then live

A version over the size limit is refused with its largest files named, so you know what to shrink or leave out. A publish over the rate limit answers rate_limited with the wait. Showing a hidden space runs the safety check again for a version still waiting on one, and each such check counts toward the hour as a publish does; past it the version keeps waiting until you publish again.

The safety check

Every version is checked before it goes live. Velven opens it in a browser as a player would, records a short take, and a model judges the take against the content policy: sexual content, scams, sign-in, payment or wallet forms, and gambling. The version's address, and every address its entry page names, may also be looked up in a list of known malware and phishing. The check usually takes a few minutes.

Outcome
Live
What happens
The version replaces the live one. The take becomes the space's clip and thumbnail.
What you do
Nothing.
Outcome
Refused
What happens
The version stays off, with the reason, and the live version keeps serving.
What you do
Fix it and publish again, or press Ask for review.
Outcome
Could not judge
What happens
The take showed a black screen, a loading screen or a start screen it never got past. The version stays off, with what the take showed.
What you do
Add a start hint to velven.json and publish again, or press Ask for review.

Follow the check on the space's Files tab, from the link the CLI prints, or wait in the terminal with velven publish --prod --wait, which exits 6 when the version is refused or could not be judged. Ask for review is on the Files tab: a person at Velven looks at the version, once per version, while no newer version is live.

Note: A newer version sent to the check while an older one waits replaces the older one, which never goes live.

Previews

A plain velven publish, or Upload a preview on the space's Files tab or the add page, makes a preview: the version, played in Velven's page at a private link on velven.ai that works for 24 hours. Anyone signed in to Velven who holds the link can open it, so a friend can test a room with you. Nothing public changes, and the preview is not checked.

Everything a preview writes, scores, saves, achievements, stats, content and rooms, goes to the space's sandbox, never the live space. velven reset clears the sandbox, and it clears itself 30 days after the last write.

To put a preview live, run velven publish --prod from the same folder, or press Publish beside it on the Files tab. Either sends it to the safety check first.

Versions and rollback

Every publish is a new numbered version. Velven keeps the live one and the last 20; older versions, and files no kept version uses, are deleted.

Terminal
velven versions      # * marks the live onevelven rollback 3    # version 3 goes live again, at once

A rollback goes only to a version that was live before, since it has already passed the check. The Files tab lists the same versions with their status; Make live beside a version that was live before rolls back to it.

Publish without an account

velven publish before you sign in, Upload files on the add page signed out, and the MCP publish tool without a sign-in all make an unlisted page: a real space page on Velven, live once it passes the check, with every SDK feature (though its achievements add no points), but on no list, search, shelf or sitemap. It shows Unclaimed and a Claim button. By publishing you agree to Velven's Terms.

  • The first publish answers with the page's address and a claim token. The CLI saves the token in velven.json as "claim", so publishing again from the folder updates the same page, checked each time.
  • The page and everything on it are deleted 7 days after its first publish unless you claim it. Publishing again does not add time.
  • --prod needs you to sign in: without an account, every publish is checked and then live on the unlisted page.

To claim it, open the claim link, https://velven.ai/claim/<token>, and sign in; or run velven login and publish again from the folder. The page becomes yours and goes on Velven, and "claim" in velven.json becomes "space".

Warning: The claim token is the key to the page: anyone who has it can update or claim it. Keep velven.json out of a public repository until you have claimed the page, or delete the claim line after claiming.

How Velven serves your files

  • / serves the entry page and /dir/ serves dir/index.html. A path with no file is a 404, or the entry page when spa is true and the path has no extension.
  • Types come from the file's extension; .wasm is served as application/wasm. A compressed build file such as game.wasm.br or data.js.gz is served as the file inside it, with its Content-Encoding, so a Unity or Godot web export works as it is.
  • Velven adds the SDK's script tag, which loads /sdk/v1.js from Velven, before the entry page's </head>, unless sdk is false or the page already loads a script from /sdk/v1.js.
  • The entry page is never cached. Every other file is revalidated by its hash on each load, so a new version reaches players on their next load and an unchanged file costs no download.
  • Each space has its own origin, so its localStorage and IndexedDB are its own. Velven.data saves belong to the space whatever its address.
  • Set cookies for the space's own host only, never for all of velven.io: another space could read or overwrite a cookie set that wide. Velven.data is the way to keep a player's progress.
  • A visitor who opens the velven.io address directly is sent to the space's Velven page. Only velven.ai may frame it, and search engines are told not to index it.

Note: Velven does not send cross-origin isolation headers, so a build that needs SharedArrayBuffer, such as a threaded Godot 4 or Unity export, does not run. Export it without threads.

Move a linked space onto Velven

A space you listed from another host can move onto Velven's hosting and keep its slug, plays, likes, boards, scores, saves and page text. Put its slug in velven.json as "space" and run velven publish --prod, or press Host it on Velven on its edit page and upload the files. The space moves when that version passes the check; until then the old address keeps playing.

To move back to a live URL, enter it under Move to a new address on the same tab; the new page needs the proof tag. A space Velven listed and nobody has claimed cannot move.

Note: The space's origin changes with a move, so anything it kept in localStorage starts empty at the new address. Velven.data saves move with it.

Link a live URL

A space already live elsewhere can be listed as it is: it stays where it is hosted, and Velven plays it in a frame and counts its plays. It can be listed when it is on a host Velven knows, names its creator in a proof tag, and lets velven.ai frame it. Velven lists spaces on these hosts and refuses any other URL with a 422. The host is read from the listed URL's address and response headers, after redirects.

Host
ChatGPT
Recognised by
A chatgpt.site address. The site must be published with “Who has access” set to “Anyone on the Internet”. A site only its owner can open answers 401, and Velven cannot check it.
Host
Vercel
Recognised by
A vercel.app address, or a custom domain whose response headers say Vercel.
Host
Netlify
Recognised by
A netlify.app address, or a custom domain whose response headers say Netlify.
Host
GitHub Pages
Recognised by
A github.io address or a custom domain served by GitHub Pages. A repository page on github.com is not a Pages site and is refused.
Host
Cloudflare
Recognised by
A workers.dev or pages.dev address only; a custom domain on Cloudflare is not recognised yet, since the server: cloudflare header proves nothing.
Host
Replit
Recognised by
A replit.app address only. A custom domain on a Replit app is not recognised, since Replit adds no header of its own; a replit.dev address is the workspace's development preview, not a deployment.
Host
Firebase
Recognised by
A web.app or firebaseapp.com address only. A custom domain on Firebase Hosting is not recognised, since Firebase adds no header of its own.

Note: Size your space to the viewport, not to a fixed ratio. On a desktop it fills the page's width at the window's height; on a phone it takes the whole screen.

The proof tag

Only a verified creator can list a space, and a space Velven listed itself is claimed the same way. The proof is one tag in the page's <head> naming your Velven handle. Velven looks for it on the live page when the space is listed or claimed. A space published on Velven needs no tag: the upload is the proof.

Add the tag below inside the <head> of the page at the listed URL.

HTML
<meta name="velven" content="@handle">
Host
ChatGPT
After adding the tag
In ChatGPT press Share, set “Who has access” to “Anyone on the Internet”, then publish again.
Host
Vercel
After adding the tag
Deploy the change.
Host
Netlify
After adding the tag
Deploy the change.
Host
GitHub Pages
After adding the tag
Push, and wait for Pages to rebuild.
Host
Cloudflare
After adding the tag
Deploy the change.
Host
Replit
After adding the tag
Publish again on Replit.
Host
Firebase
After adding the tag
Deploy again with firebase deploy.

Deploy first and wait for the host's cache. A check that comes too early answers 409 unverified, with the change still needed. The older proof, a /.well-known/velven file at the site's origin containing @handle, is still accepted.

Allow Velven to frame your page

Every space plays inside the Velven page, with no option to open it in a new tab instead, so its host must let velven.ai frame it. All 7 hosts allow that by default, so most pages need nothing. A page that sends its own headers must not send X-Frame-Options, and its Content-Security-Policy must name velven.ai in frame-ancestors:

HTTP
Content-Security-Policy: frame-ancestors 'self' https://velven.ai

Velven reads the headers when the space is listed, and again on its background check every 6 hours. The agent API refuses a page that blocks framing with 422 unframeable and the change for its host. A listed space whose host starts refusing frames leaves Velven until the header is back; its creator sees the fix on the space's page and its edit page, and can press Check again on either.

ChatGPT

A ChatGPT site sends no headers of its own and always allows framing. Nothing to do.

Vercel

Add this to vercel.json at the project root, merged into any headers list already there. A Next.js app can set the same header in next.config instead.

JSON
{  "headers": [    {      "source": "/(.*)",      "headers": [        {          "key": "Content-Security-Policy",          "value": "frame-ancestors 'self' https://velven.ai"        }      ]    }  ]}

Remove any X-Frame-Options header the app sets (a security preset, a middleware).

Deploy the change.

Netlify

Add this to a _headers file in the publish directory, or the same rule under [[headers]] in netlify.toml.

Text
/*  Content-Security-Policy: frame-ancestors 'self' https://velven.ai

Remove any X-Frame-Options header the app sets (a security preset, a middleware).

Deploy the change.

GitHub Pages

GitHub Pages sets no headers and always allows framing. Nothing to do; a page that refuses framing there is being blocked by something in front of it.

Cloudflare

On Pages, or a Worker that serves static assets, add this to a _headers file in the output (assets) directory. On a Worker whose fetch handler builds the page, set the header on the response there.

Text
/*  Content-Security-Policy: frame-ancestors 'self' https://velven.ai

Remove any X-Frame-Options header the app sets (a security preset, a middleware).

Deploy the change.

Replit

On a static deployment, add this to the .replit file at the project root. On an Autoscale or Reserved VM deployment, set the header on the response in the server.

TOML
[[deployment.responseHeaders]]path = "/*"name = "Content-Security-Policy"value = "frame-ancestors 'self' https://velven.ai"

Remove any X-Frame-Options header the app sets (a security preset, a middleware).

Publish again.

Firebase

Add this to firebase.json at the project root, merged into the hosting block and any headers list already there.

JSON
{  "hosting": {    "headers": [      {        "source": "**",        "headers": [          {            "key": "Content-Security-Policy",            "value": "frame-ancestors 'self' https://velven.ai"          }        ]      }    ]  }}

Remove any X-Frame-Options header the app sets (a security preset, a middleware).

Deploy again with firebase deploy.

What Velven checks on a linked space, and when

Velven checks a linked page when it is listed, when it is claimed, every 6 hours in the background, and when you press a button for it:

When
Listing, on the add page or POST /api/spaces
What
The host, that the page answers, the proof, the framing headers, the page's boards block, and it may be checked against the malware and phishing list.
When
A claim, at /s/{slug}/claim or POST /api/spaces/verify
What
The proof.
When
A move, on the edit page's Settings tab or POST /api/spaces/{slug}/move
What
The new page as a listing checks it: the host, that it answers with a page and does not ask visitors to sign in, the framing headers, and, on a claimed space, the proof and its boards block; it may also be checked against the malware and phishing list.
When
Every 6 hours, in the background
What
That the page still answers and allows framing, its boards block, and whether it changed; it may also be checked against the malware and phishing list. A page that changed gets the same safety check as a hosted version, at most once a day, and one judged unsafe is hidden until Velven reviews it.
When
Check again, on the space's page and its edit page while its host refuses frames
What
The same as the background check, at once. A page that allows framing again goes back on Velven.
When
Check my page now, on the edit page's Leaderboards tab
What
The boards block, at once.

List a space with an agent has the API calls, and Leaderboards the boards block.

Move a linked space to another host

A space that moves to a new URL keeps its listing: its slug, plays, likes, boards, scores, saves and page text stay. Put the proof tag on the new page and allow framing there, deploy, then enter the new URL under Move to a new address, on the Settings tab of the space's edit page.

An agent moves it with POST /api/spaces/{slug}/move; Move to another host has the call and what to update on the new host, and the REST API every refusal.

Note: A server that posts scores or checks identity tokens needs the move too: a board secret in the new host's environment, and the new origin as the tokens' audience.