Concepts, value, and typical clone scenarios — less code.
Concepts, value, and typical clone scenarios — less code.
Підготовка контенту платформи Ring
Підготовка контенту платформи Ring
Підготовка контенту платформи Ring
Use Founder / Developer tabs in the docs sidebar to filter this page. Sidebar visibility is curated in lib/docs/audience-curated-docs.ts.
Ring ships peer mini-games that reuse Tunnel fan-out, the interactive-message kit, and public-profile IA — without a GameConductor, parallel messaging stack, or stakes.
| Surface | Path |
|---|---|
| Marketplace | /games |
| Mini-app | /games/[slug]?session= |
| Public availability | /{username}/games |
| Owner manage | /profile/games (Member) |
| Chat widget | message type / metadata.kind = game_request |
| Incoming banner | MessagesShell and /games layout |
Titles (shipped): tic-tac-toe · chess · checkers. Catalog copy is localized via locales/*/modules/games.json (modules.games); English strings in catalog.ts remain FCM/history fallback.
Members bond faster when they can play without leaving your Ring. Challenges land as chat widgets and as a global IncomingGameBanner on Messages and /games — so invites are not missed outside the messenger thread.
/{username}/games/profile/games and publish availability (chess / checkers / tic-tac-toe)./games in each locale — catalog titles should localize (EN/UK/RU/ES/DE).Depends-on: game_request lives in the interactive-message kit and direct conversations.
Same-workflow: shared call/game mutex; ICE route reused for DataChannel move hints.
Depends-on: conversation:*, game:{sessionId} ACL, and games:incoming fan-out.
Prerequisite: Member privileges gate /profile/games and challenge compose.
Use Founder / Developer tabs in the docs sidebar to filter this page. Sidebar visibility is curated in lib/docs/audience-curated-docs.ts.
Ring ships peer mini-games that reuse Tunnel fan-out, the interactive-message kit, and public-profile IA — without a GameConductor, parallel messaging stack, or stakes.
| Surface | Path |
|---|---|
| Marketplace | /games |
| Mini-app | /games/[slug]?session= |
| Public availability | /{username}/games |
| Owner manage | /profile/games (Member) |
| Chat widget | message type / metadata.kind = game_request |
| Incoming banner | MessagesShell and /games layout |
Titles (shipped): tic-tac-toe · chess · checkers. Catalog copy is localized via locales/*/modules/games.json (modules.games); English strings in catalog.ts remain FCM/history fallback.
Members bond faster when they can play without leaving your Ring. Challenges land as chat widgets and as a global IncomingGameBanner on Messages and /games — so invites are not missed outside the messenger thread.
/{username}/games/profile/games and publish availability (chess / checkers / tic-tac-toe)./games in each locale — catalog titles should localize (EN/UK/RU/ES/DE).Depends-on: game_request lives in the interactive-message kit and direct conversations.
Same-workflow: shared call/game mutex; ICE route reused for DataChannel move hints.
Depends-on: conversation:*, game:{sessionId} ACL, and games:incoming fan-out.
Prerequisite: Member privileges gate /profile/games and challenge compose.
Use Founder / Developer tabs in the docs sidebar to filter this page. Sidebar visibility is curated in lib/docs/audience-curated-docs.ts.
Ring ships peer mini-games that reuse Tunnel fan-out, the interactive-message kit, and public-profile IA — without a GameConductor, parallel messaging stack, or stakes.
| Surface | Path |
|---|---|
| Marketplace | /games |
| Mini-app | /games/[slug]?session= |
| Public availability | /{username}/games |
| Owner manage | /profile/games (Member) |
| Chat widget | message type / metadata.kind = game_request |
| Incoming banner | MessagesShell and /games layout |
Titles (shipped): tic-tac-toe · chess · checkers. Catalog copy is localized via locales/*/modules/games.json (modules.games); English strings in catalog.ts remain FCM/history fallback.
Members bond faster when they can play without leaving your Ring. Challenges land as chat widgets and as a global IncomingGameBanner on Messages and /games — so invites are not missed outside the messenger thread.
/{username}/games/profile/games and publish availability (chess / checkers / tic-tac-toe)./games in each locale — catalog titles should localize (EN/UK/RU/ES/DE).Depends-on: game_request lives in the interactive-message kit and direct conversations.
Same-workflow: shared call/game mutex; ICE route reused for DataChannel move hints.
Depends-on: conversation:*, game:{sessionId} ACL, and games:incoming fan-out.
Prerequisite: Member privileges gate /profile/games and challenge compose.
Conversations and interactive widgets (game_request in the shared kit).
features/peer-games/hooks/use-peer-game-datachannel.ts | Optimistic move hints (ring-peer-game-moves) |
app/_actions/peer-games.ts | Server actions |
app/api/conversations/[id]/game-invite/route.ts | Thin HTTP twin → createInvite |
app/api/cron/peer-game-session-expiry/route.ts | ProcessConductor pipeline |
app/api/tunnel/subscribe/route.ts | HTTP game:* participant ACL |
lib/tunnel/native-ws/attach.ts | WS game:* participant ACL (mirrors HTTP) |
IncomingGameBanner | games:incoming + game:{id} lifecycle clear |
Migration data/migrations/043_peer_games.sql:
peer_game_sessions — indexes on conversation / challenger / peer / status / slug / messageId / updated_atuser_peer_games — per-owner enabledSlugs (unique on ownerId)| Channel | Events |
|---|---|
conversation:{id} | game:invite, game:accept, game:decline, game:resign, game:move, game:expire |
game:{sessionId} | game:accept, game:decline, game:resign, game:move, game:expire, game:dc-signal |
| user inbox | games:incoming (invite, accepted, terminal) |
Subscribe ACL: deny-by-default for game:{sessionId} on HTTP and native WS — only DB participants (getSessionForParticipant). Spectate deferred.
Banner clear: game:expire / game:decline / game:accept on the session channel, plus games:incoming with terminal or accepted (covers subscribe race after invite).
NotificationType.GAME_REQUEST / GAME_UPDATED. Offline push: after !deliveredLive, wait ~500ms and recheck getTunnelHub().isUserConnected(peer) before createNotification + FCM. Soft Web Audio chime on the banner only — never call ringtone / setPeerCallBusy.
Hub isUserConnected is process-local. Multi-pod false offline FCM is a known residual — shared presence is backlog.
setPeerCallBusy / setPeerGameBusy with BroadcastChannel (+ optional navigator.locks)SET NX PX inside createInvite (claim-before; releaseNx on failure) — Server Action + HTTP + Play-with-me share SSOTkey={fen}); drops return falsePipeline peer-game-session-expiry:
| Case | TTL | Result |
|---|---|---|
Orphan pending (no messageId) | ~2m | Reclaim |
| Pending invite | 15m | Declined + updateMessageLocked |
| Active idle | 2h | Resigned/completed + Tunnel notify |
Schedulers: Vercel */5 (vercel.json) · k3s-or CronJobs infrastructure/k3s-or/ring-platform-org/cronjob-peer-game-session-expiry.yaml (+ sibling cronjob-close-expired-polls.yaml, /bin/sh so CRON_SECRET expands). Apply on cluster when promoting k8s prod.
RTCDataChannel label ring-peer-game-moves (ordered) + fetchIceServers. Peer envelopes are hints; validateMove / Tunnel game:move / DB remain SSOT. Reject → rollback to last authoritative hydrate. ICE restart under flaky nets is backlog; Tunnel path remains the fallback.
createGameRequest · acceptGameRequest · declineGameRequest · resignPeerGameAction · submitPeerGameMoveAction · getPeerGameSessionAction · updateEnabledGamesAction · challengeUserToGameAction
Structural: npx tsx scripts/smoke-peer-games-soak.cts. Plugin unit tests under __tests__/features/peer-games/plugins.test.ts. Live Member E2E checklist: AI-CONTEXT/concepts/peer-games-live-e2e-soak-2026-07-24.json (empire Reggie propagate blocked until signoff).
Next-step: public /{username}/games beside Player and Gallery.
createGameRequest / POST …/game-invite / challengeUserToGameAction
→ PeerGameService.createInvite (SSOT)
→ Redis SET NX claim peer-game:invite:{from}:{to}:{slug} (Map fallback)
→ create session (peer_game_sessions) FIRST
→ send game_request message → link messageId
→ on failure: delete session + releaseNx
→ publishToChannel(conversation:…, game:invite)
→ publishToUserTunnel(peer, games:incoming)
→ if !deliveredLive: sleep ~500ms; FCM GAME_REQUEST only if still !isUserConnected
accept / decline / resign / submitMove
→ participant ACL + plugin validateMove/applyMove
→ updateMessageLocked on linked widget metadata
→ game:* on game:{sessionId} AND conversation:{id}
→ decline/expire also publish terminal on games:incoming (banner clear)
client mini-app
→ hydrate DB SSOT → useTunnelChannel(game:{id})
→ optional RTCDataChannel optimistic hints (fetchIceServers); server remains SSOTConversations and interactive widgets (game_request in the shared kit).
features/peer-games/hooks/use-peer-game-datachannel.ts | Optimistic move hints (ring-peer-game-moves) |
app/_actions/peer-games.ts | Server actions |
app/api/conversations/[id]/game-invite/route.ts | Thin HTTP twin → createInvite |
app/api/cron/peer-game-session-expiry/route.ts | ProcessConductor pipeline |
app/api/tunnel/subscribe/route.ts | HTTP game:* participant ACL |
lib/tunnel/native-ws/attach.ts | WS game:* participant ACL (mirrors HTTP) |
IncomingGameBanner | games:incoming + game:{id} lifecycle clear |
Migration data/migrations/043_peer_games.sql:
peer_game_sessions — indexes on conversation / challenger / peer / status / slug / messageId / updated_atuser_peer_games — per-owner enabledSlugs (unique on ownerId)| Channel | Events |
|---|---|
conversation:{id} | game:invite, game:accept, game:decline, game:resign, game:move, game:expire |
game:{sessionId} | game:accept, game:decline, game:resign, game:move, game:expire, game:dc-signal |
| user inbox | games:incoming (invite, accepted, terminal) |
Subscribe ACL: deny-by-default for game:{sessionId} on HTTP and native WS — only DB participants (getSessionForParticipant). Spectate deferred.
Banner clear: game:expire / game:decline / game:accept on the session channel, plus games:incoming with terminal or accepted (covers subscribe race after invite).
NotificationType.GAME_REQUEST / GAME_UPDATED. Offline push: after !deliveredLive, wait ~500ms and recheck getTunnelHub().isUserConnected(peer) before createNotification + FCM. Soft Web Audio chime on the banner only — never call ringtone / setPeerCallBusy.
Hub isUserConnected is process-local. Multi-pod false offline FCM is a known residual — shared presence is backlog.
setPeerCallBusy / setPeerGameBusy with BroadcastChannel (+ optional navigator.locks)SET NX PX inside createInvite (claim-before; releaseNx on failure) — Server Action + HTTP + Play-with-me share SSOTkey={fen}); drops return falsePipeline peer-game-session-expiry:
| Case | TTL | Result |
|---|---|---|
Orphan pending (no messageId) | ~2m | Reclaim |
| Pending invite | 15m | Declined + updateMessageLocked |
| Active idle | 2h | Resigned/completed + Tunnel notify |
Schedulers: Vercel */5 (vercel.json) · k3s-or CronJobs infrastructure/k3s-or/ring-platform-org/cronjob-peer-game-session-expiry.yaml (+ sibling cronjob-close-expired-polls.yaml, /bin/sh so CRON_SECRET expands). Apply on cluster when promoting k8s prod.
RTCDataChannel label ring-peer-game-moves (ordered) + fetchIceServers. Peer envelopes are hints; validateMove / Tunnel game:move / DB remain SSOT. Reject → rollback to last authoritative hydrate. ICE restart under flaky nets is backlog; Tunnel path remains the fallback.
createGameRequest · acceptGameRequest · declineGameRequest · resignPeerGameAction · submitPeerGameMoveAction · getPeerGameSessionAction · updateEnabledGamesAction · challengeUserToGameAction
Structural: npx tsx scripts/smoke-peer-games-soak.cts. Plugin unit tests under __tests__/features/peer-games/plugins.test.ts. Live Member E2E checklist: AI-CONTEXT/concepts/peer-games-live-e2e-soak-2026-07-24.json (empire Reggie propagate blocked until signoff).
Next-step: public /{username}/games beside Player and Gallery.
createGameRequest / POST …/game-invite / challengeUserToGameAction
→ PeerGameService.createInvite (SSOT)
→ Redis SET NX claim peer-game:invite:{from}:{to}:{slug} (Map fallback)
→ create session (peer_game_sessions) FIRST
→ send game_request message → link messageId
→ on failure: delete session + releaseNx
→ publishToChannel(conversation:…, game:invite)
→ publishToUserTunnel(peer, games:incoming)
→ if !deliveredLive: sleep ~500ms; FCM GAME_REQUEST only if still !isUserConnected
accept / decline / resign / submitMove
→ participant ACL + plugin validateMove/applyMove
→ updateMessageLocked on linked widget metadata
→ game:* on game:{sessionId} AND conversation:{id}
→ decline/expire also publish terminal on games:incoming (banner clear)
client mini-app
→ hydrate DB SSOT → useTunnelChannel(game:{id})
→ optional RTCDataChannel optimistic hints (fetchIceServers); server remains SSOTConversations and interactive widgets (game_request in the shared kit).
features/peer-games/hooks/use-peer-game-datachannel.ts | Optimistic move hints (ring-peer-game-moves) |
app/_actions/peer-games.ts | Server actions |
app/api/conversations/[id]/game-invite/route.ts | Thin HTTP twin → createInvite |
app/api/cron/peer-game-session-expiry/route.ts | ProcessConductor pipeline |
app/api/tunnel/subscribe/route.ts | HTTP game:* participant ACL |
lib/tunnel/native-ws/attach.ts | WS game:* participant ACL (mirrors HTTP) |
IncomingGameBanner | games:incoming + game:{id} lifecycle clear |
Migration data/migrations/043_peer_games.sql:
peer_game_sessions — indexes on conversation / challenger / peer / status / slug / messageId / updated_atuser_peer_games — per-owner enabledSlugs (unique on ownerId)| Channel | Events |
|---|---|
conversation:{id} | game:invite, game:accept, game:decline, game:resign, game:move, game:expire |
game:{sessionId} | game:accept, game:decline, game:resign, game:move, game:expire, game:dc-signal |
| user inbox | games:incoming (invite, accepted, terminal) |
Subscribe ACL: deny-by-default for game:{sessionId} on HTTP and native WS — only DB participants (getSessionForParticipant). Spectate deferred.
Banner clear: game:expire / game:decline / game:accept on the session channel, plus games:incoming with terminal or accepted (covers subscribe race after invite).
NotificationType.GAME_REQUEST / GAME_UPDATED. Offline push: after !deliveredLive, wait ~500ms and recheck getTunnelHub().isUserConnected(peer) before createNotification + FCM. Soft Web Audio chime on the banner only — never call ringtone / setPeerCallBusy.
Hub isUserConnected is process-local. Multi-pod false offline FCM is a known residual — shared presence is backlog.
setPeerCallBusy / setPeerGameBusy with BroadcastChannel (+ optional navigator.locks)SET NX PX inside createInvite (claim-before; releaseNx on failure) — Server Action + HTTP + Play-with-me share SSOTkey={fen}); drops return falsePipeline peer-game-session-expiry:
| Case | TTL | Result |
|---|---|---|
Orphan pending (no messageId) | ~2m | Reclaim |
| Pending invite | 15m | Declined + updateMessageLocked |
| Active idle | 2h | Resigned/completed + Tunnel notify |
Schedulers: Vercel */5 (vercel.json) · k3s-or CronJobs infrastructure/k3s-or/ring-platform-org/cronjob-peer-game-session-expiry.yaml (+ sibling cronjob-close-expired-polls.yaml, /bin/sh so CRON_SECRET expands). Apply on cluster when promoting k8s prod.
RTCDataChannel label ring-peer-game-moves (ordered) + fetchIceServers. Peer envelopes are hints; validateMove / Tunnel game:move / DB remain SSOT. Reject → rollback to last authoritative hydrate. ICE restart under flaky nets is backlog; Tunnel path remains the fallback.
createGameRequest · acceptGameRequest · declineGameRequest · resignPeerGameAction · submitPeerGameMoveAction · getPeerGameSessionAction · updateEnabledGamesAction · challengeUserToGameAction
Structural: npx tsx scripts/smoke-peer-games-soak.cts. Plugin unit tests under __tests__/features/peer-games/plugins.test.ts. Live Member E2E checklist: AI-CONTEXT/concepts/peer-games-live-e2e-soak-2026-07-24.json (empire Reggie propagate blocked until signoff).
Next-step: public /{username}/games beside Player and Gallery.
createGameRequest / POST …/game-invite / challengeUserToGameAction
→ PeerGameService.createInvite (SSOT)
→ Redis SET NX claim peer-game:invite:{from}:{to}:{slug} (Map fallback)
→ create session (peer_game_sessions) FIRST
→ send game_request message → link messageId
→ on failure: delete session + releaseNx
→ publishToChannel(conversation:…, game:invite)
→ publishToUserTunnel(peer, games:incoming)
→ if !deliveredLive: sleep ~500ms; FCM GAME_REQUEST only if still !isUserConnected
accept / decline / resign / submitMove
→ participant ACL + plugin validateMove/applyMove
→ updateMessageLocked on linked widget metadata
→ game:* on game:{sessionId} AND conversation:{id}
→ decline/expire also publish terminal on games:incoming (banner clear)
client mini-app
→ hydrate DB SSOT → useTunnelChannel(game:{id})
→ optional RTCDataChannel optimistic hints (fetchIceServers); server remains SSOT