Repositories openvibers openvibe.play official
openvibers/openvibe.play
Open source achievements and leaderboards for every OpenVibe game, at play.openvibe.network.
Get it
Get this repository
A Git token is made for this copy and shown once. It can read and write the repositories you own, and expires in an hour.
git clone https://git.openvibe.codes/openvibers/openvibe.play.git
OpenVibe Link asks you on your computer. The folder is inside the root you chose in Link, and the request ends after two minutes.
Clone to my computerA Codes session clones from our disk: no network, no GitHub quota. When this repository has a GitHub twin, Codes pushes there too.
Open in CodesREADME.md
View fileOpenVibe.Play
Achievements, leaderboards and cloud saves for every OpenVibe game. https://play.openvibe.network
Live site: https://play.openvibe.network · API docs: https://openvibe.services · For AI: https://play.openvibe.network/llms.txt
Run it: npm ci && npm run dev (Node 22 or newer) · Tests: npm test
OpenVibe.Play is the network's game services (OpenVibe.Games ADR-0007
decision 10). A game registers its title with its own OpenVibe token, posts what its players did, and anyone reads
its boards. The rules of each game stay in the game; Play keeps the record and answers it.
- Titles belong to the principal that registered them (a game's service or app token). Only that principal writes to
a title; to anyone else it does not exist. - Your own game, from the sandbox (ADR-014 opt-in): any developer makes a project on openvibe.services and a sandbox
app with Play's three capabilities (in Network's default sandbox allowance). A sandbox app registers up to 3 titles
whose ids start withsandbox-, with guest players only (nothing adopted, no person's record touched), never listed,
counted or indexed: readable at their id, markedsandbox: true, and deleted by their app (DELETE /titles/:title;
a production title is never deleted). A production app (staff turn production on for the
project) registers the real name; it never takes asandbox-id. - Achievements, including hidden ones whose name shows only once unlocked. A definition left out of an update is
retired, never deleted: what players earned stays. - Leaderboards of three kinds.
bestranks each player's best score.sumranks the total of theirs.time
ranks each player's fastest run, in whole milliseconds (at most 24 hours), fastest first. Each reads this week (from
Monday 00:00 UTC) or all time, 25 a page, and a tie goes to whoever got there first. A board'sunitsays what its
scores mean:"ms"fortime,"points"for the others. - Players are people (
usr_…) or guests a game names (guest:<opaque>). When a guest signs in, the game adopts
them and their unlocks and scores become the person's. - Cloud saves (
migrations/0004_saves.sql): a player's progress follows them to any device. Up to 8 slots per
player per title, each any JSON value of at most 64 KB, versioned: a write names the version it read and a stale one
is refused (409play.save_conflict). The title's own token writes (play.result.write) and reads
(play.player.read); a person lists and deletes their own at/me/savesand sees them on /me. A guest's slots move
to the person on adoption (the newer save wins a clash); they are in the account export and erased with the account. - Idempotent writes. A round's scores carry a
ref; posting it again changes nothing. An unlock answers only the
achievements that were new. - Personal data. Only people's rows are: they are in the person's account export and erased with their account
(ADR-033). A board shows a name and a score, never a player id.
Every page is server-rendered and readable without JavaScript: the home (every game and this week's leaders), a title,
a board, your achievements and the API docs.
Purpose
One record of what players did in OpenVibe games, shared by every game and read anywhere. A game keeps its own rules and
reports their results; Play stores the achievements unlocked and the scores posted, ranks them, and serves them to the
game, to the player's account page and to any page that wants a board.
Owns
- Titles, their achievement definitions and their boards (
play_titles,play_achievements,play_boards). - Unlocks, scores and cloud saves per player (
play_unlocks,play_scores,play_saves) and guest adoption. - The
/api/v1read and write API. The player pages now live on OpenVibe.Games (achievements, leaderboards, developers); every old page at play.openvibe.network answers 301 to its counterpart there, and the API stays here. - Request telemetry (
telemetry_samples,0003_telemetry.sql): each HTTP request aggregated by route template, method, status class,
never by raw URL;/shared, static assets, health, readiness and/metricsare not counted.server/app.jsbuilds the collector,
server/index.jsflushes per key onehttp.requestsample plus the active-requests, p95 latency and event-loop-lag gauges each
minute and on stop (openvibe-sdk 0.46,platform.telemetry-sample@1), and rows older than 14 days are deleted daily.
Does not own
- A game's rules, rounds or anti-cheat: the game decides what counts and posts the result. Games' local tables stay in
OpenVibe.Games, which posts to Play. - People's accounts, names shown elsewhere, or sign-in: OpenVibe.Network. Rewards worn across the network (items) are
OpenVibe.Inventory's; OpenCoins are the Network wallet's.
Depends on
- OpenVibe.Network: service and app tokens (audience
openvibe.play), the JWKS that verifies them, grants for the
play.*capabilities, and person sign-in for "your achievements". - OpenVibe.Events: the account subscriptions (export and deletion), created at boot when
EVENTS_URL,PLAY_EVENTS_SECRETand the client secret are set. - PostgreSQL (
ov_play; embedded PGlite in development and tests), Valkey for per-caller limits when configured.
Capabilities
| Capability | Lets a caller |
|---|---|
| play.title.manage | register or update its own title (name, url, summary, achievements, boards) |
| play.result.write | post scores and unlocks to its own title, and adopt a guest as a person |
| play.player.read | read a player's unlocks in its own title |
A title belongs to the principal that registered it; another principal's write is refused as if the title did not
exist. Reads of titles and boards are public.
Acceptance
Tested in test/ (npm test, every file in its own process on PGlite; npm run test:pg on PostgreSQL):
play-api.test.js (titles, owner-only writes, idempotent scores, best and sum boards with the Monday week, guest
adoption, secret achievements, retired definitions, a person's own achievements, the API), moved-pages.test.js
(the page redirects and the crawl files), time-boards.test.js
(time boards, their ties, validation and units), time-migration.test.js (the kind CHECK widening over existing
boards), account-data.test.js
(export and erasure), the auth, limits, redirects, open-redirect, nginx and security tests.
Security
- Writes need a Network token with the capability, for audience
openvibe.play; a principal writes only its own titles. - Every write is idempotent (
reffor scores, set semantics for unlocks) and limited per calling principal
(http/caller-limits.js), sign-in routes per address. - Boards show a name and a score, never a player id; guests are opaque ids chosen by the game.
- Report vulnerabilities as [SECURITY.md](SECURITY.md) says.
Deploy
sudo ovhost deploy play on the host: unit openvibe-play.service on 127.0.0.1:5040, env /etc/openvibe/play.env,
database ov_play, nginx vhost deploy/nginx/play.openvibe.network.conf on the openvibe.network wildcard certificate.
Migrations run at boot.
API (/api/v1, problem+json errors)
| Route | Who | What |
|---|---|---|
| GET /titles, GET /titles/:title | anyone (CORS) | the games, their achievements (with how many players have each) and boards |
| GET /titles/:title/boards/:board?period=week\|all&page= | anyone (CORS) | a board: rank, name, score, signed_in, unit (ms for a time board, points otherwise) |
| GET /people/:subject/achievements | anyone (CORS) | a person's unlocks in every game, { subject, total, titles }: their profile on OpenVibe.Network and any site's showcase (only people, never a guest) |
| GET /me/achievements | a person | their unlocks in every game |
| PUT /titles/:title | play.title.manage | register or update a title: name, url, summary, achievements, boards |
| POST /titles/:title/scores | play.result.write | { board, ref, entries: [{ player, name, score }] }, up to 100; a time board's score is whole milliseconds from 0 to 86400000 |
| POST /titles/:title/unlocks | play.result.write | { player, achievements: [id] } → the new ones |
| POST /titles/:title/players/:guest/adopt | play.result.write | { person: "usr_…" }: a guest signed in |
| GET /titles/:title/players/:player/achievements | play.player.read | a player's unlocks in your title |
Tokens are Network service or app tokens for audience openvibe.play; the contracts are play.* in
openvibe-contracts (0.172.1).
Run
npm install
npm run dev # http://localhost:5040, embedded PGlite in data/pglite
npm test # every test on PGlite; npm run test:pg on PostgreSQL
Configuration is environment only (.env.example, names only). Production: unit openvibe-play.service on
127.0.0.1:5040, database ov_play, nginx vhost deploy/nginx/play.openvibe.network.conf on the openvibe.network
wildcard certificate, deployed with sudo ovhost deploy play.
Layout
server/play/store.js titles, achievements, boards, scores, unlocks, adoption
server/play/account-data.js account export and deletion (ADR-033)
server/http/api.js /api/v1
server/http/moved.js the old page addresses, 301 to OpenVibe.Games
server/http/discovery.js robots.txt and llms.txt (sitemap.xml and llms-full.txt redirect to Games)
migrations/ the schema (0001 the store, 0002 the retired merges table)
License
AGPL-3.0. Part of the OpenVibe network.
<!-- versions:start -->
- openvibe-contracts: v0.419.0
- openvibe-sdk: v0.161.0
- openvibe-shared: v4.235.0
<!-- versions:end -->