Package Exports
- @vibecade/sdk
- @vibecade/sdk/package.json
Readme
@vibecade/sdk
What it is
@vibecade/sdk is a JavaScript library for integrating with the
Vibecade platform. It exposes a single class,
Vibecade, whose methods let a web page:
- Sign players in (anonymously by default) and read their profile.
- Submit scores and read leaderboards.
- Unlock achievements and record progress toward progressive ones.
- Read and write a per-player per-game cloud save blob.
- Emit analytics events.
- Promote an anonymous account to a claimed one (email / OAuth).
The SDK is optional. A web page that never loads it is still a perfectly valid consumer of the platform — the platform only requires that games be reachable at a URL. The SDK is what lets a game opt into platform features.
Loading the SDK
@vibecade/sdk is published in three forms. A web page can load it via
any of them:
1. Via <script> tag from the CDN
This form requires no build tooling. Load Supabase first (the SDK's
peer dependency) and then the SDK. The SDK exposes the Vibecade
class on window.
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.js"></script>
<script src="https://sdk.vibecade.ai/v1/sdk.js"></script>
<script>
const vc = new Vibecade({
gameId: 'your-game-id',
apiKey: 'your-api-key',
});
</script>2. As an ES module from the CDN
<script type="module">
import { Vibecade } from 'https://sdk.vibecade.ai/v1/sdk.esm.js';
const vc = new Vibecade({
gameId: 'your-game-id',
apiKey: 'your-api-key',
});
</script>3. Via npm
npm install @vibecade/sdk @supabase/supabase-jsimport { Vibecade } from '@vibecade/sdk';
const vc = new Vibecade({
gameId: 'your-game-id',
apiKey: 'your-api-key',
});Quickstart
Shortest path from "SDK loaded" to "first leaderboard entry":
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.js"></script>
<script src="https://sdk.vibecade.ai/v1/sdk.js"></script>
<script>
const vc = new Vibecade({
gameId: 'your-game-id',
apiKey: 'your-api-key',
});
async function run() {
await vc.init();
console.log('playing as', vc.getPlayer());
const result = await vc.submitScore({ score: 1234 });
console.log('rank', result.rank);
const top = await vc.getLeaderboard({ limit: 10 });
for (const entry of top) {
console.log(entry.rank, entry.player.displayName, entry.score);
}
}
run().catch((err) => console.error(err.code, err.message));
</script>That's the full happy path. No accounts created up front; init()
signs the player in anonymously, the DB creates a profile, and the
score carries their rank.
API reference
All methods return Promises unless stated otherwise. Every call should
be wrapped in try/catch — see Error handling.
new Vibecade(config)
Construct an SDK instance. Does not perform any network work — call
init() to establish a session.
Parameters:
| Field | Type | Notes |
|---|---|---|
gameId |
string — required |
Slug of your game on the platform. |
apiKey |
string — required |
Your game's API key. |
apiUrl |
string — optional |
Override for dev/staging. Defaults to the production API. |
anonKey |
string — optional |
Supabase anon key for the target environment. Required when apiUrl points at anything other than production. |
debug |
boolean — optional |
When true, SDK logs internals via console.debug. |
Throws VibecadeError with code INVALID_CONFIG if required fields
are missing or wrong type.
init(): Promise<void>
Establish a player session. On first call with no prior session, signs in anonymously and creates a player profile server-side. On subsequent calls (or when a saved session is still valid), attaches to the existing session. Idempotent: concurrent calls share one in-flight request; repeat calls after success return immediately.
Must be awaited before any method that requires a signed-in player.
Throws VibecadeError with code AUTH_FAILED if the sign-in
round-trip fails.
isAuthenticated(): boolean
Synchronous. Returns true when the SDK currently holds a session.
Safe to use in render paths.
getPlayer(): Player | null
Synchronous. Returns the cached Player object (populated by init()
and kept current by auth state changes) or null before init.
interface Player {
id: string;
displayName: string;
avatarUrl: string | null;
isAnonymous: boolean;
createdAt: string;
}startPlay(): Promise<{ playToken: string }>
Ask the platform for a one-use anti-cheat play token. Games whose
platform entry has requires_play_token = true must call this at the
start of a play session and pass the returned playToken to
submitScore at the end. See Anti-cheat guidance.
Tokens expire 10 minutes after issuance.
submitScore(opts): Promise<SubmitScoreResult>
Submit a score for the current player.
vc.submitScore({
score: 1234,
meta: { level: 3, seed: 'abc' }, // optional
});Returns { accepted: true, id: number, rank: number }. rank is the
player's position on the all-time leaderboard for this game, counted
as the number of strictly-better scores plus one.
Play tokens are handled automatically. If the game has
requires_play_token = true set, the SDK transparently calls
startPlay() and retries — no manual token management needed.
Tokens are single-use, so this costs one extra round-trip per
submit on those games. If you'd rather manage tokens explicitly
(e.g., pre-fetching for latency), pass opts.playToken and the
SDK uses your token without auto-fetching:
const { playToken } = await vc.startPlay();
// ... gameplay ...
vc.submitScore({ score: 1234, playToken });Common error codes: INVALID_API_KEY, SCORE_OUT_OF_RANGE (exceeds
the game's max_plausible_score), PLAY_TOKEN_EXPIRED,
PLAY_TOKEN_USED. (PLAY_TOKEN_REQUIRED is no longer thrown — the
SDK auto-recovers before the error reaches the caller.)
getLeaderboard(opts?): Promise<ScoreEntry[]>
Fetch a page of scores for the current game.
vc.getLeaderboard({
scope: 'all-time', // 'global' | 'today' | 'weekly' | 'all-time'
limit: 20, // default 20, max 100
around: 'me', // optional — centers the page on the player's rank
});Returns an array of entries ordered by the game's configured direction (descending for higher-is-better games, ascending for lower-is-better):
interface ScoreEntry {
id: number;
score: number;
createdAt: string;
meta: Record<string, unknown> | null;
player: { id: string; displayName: string; avatarUrl: string | null };
rank: number;
}When around: 'me' is set and the player has no score in the scope,
falls back to the top of the board.
unlockAchievement(achievementId): Promise<UnlockResult>
Unlock a one-shot achievement for the current player. Idempotent: re-unlocking preserves the original timestamp.
const res = await vc.unlockAchievement('FIRST_CUP');
// { newlyUnlocked: boolean, unlockedAt: string }Throws ACHIEVEMENT_WRONG_TYPE if the id refers to a progressive
achievement — use setAchievementProgress for those.
setAchievementProgress(achievementId, progress): Promise<ProgressResult>
Record progress toward a progressive achievement. Progress is monotonic and max-wins — a value below the stored progress is silently clamped to the stored value (safe for retries after flaky networks); equal is an idempotent no-op. When progress reaches or passes the achievement's target for the first time, the achievement unlocks.
const res = await vc.setAchievementProgress('CUP_CENTURY', 42);
// { newlyUnlocked: boolean, progress: number, target: number }Throws ACHIEVEMENT_WRONG_TYPE if the id refers to a one-shot
achievement — use unlockAchievement for those.
getAchievements(): Promise<AchievementProgress[]>
Return every achievement defined for the game, with the current player's progress folded in.
interface AchievementProgress {
id: string;
displayName: string;
description: string | null;
iconUrl: string | null;
points: number;
isProgressive: boolean;
targetProgress: number | null;
progress: number;
unlocked: boolean;
unlockedAt: string | null;
}cloudSave.get<T>(): Promise<T | null>
Read the current player's save blob for this game. Returns null if
none was saved yet.
cloudSave.set<T>(data: T): Promise<void>
Write (upsert) the current player's save blob for this game.
Cloud saves are owner-only at the DB level — other players cannot read or write your save.
trackEvent(name, properties?): void
Synchronous, fire-and-forget. Buffers an analytics event in memory.
The buffer flushes on a 5-second interval, when it reaches 20 events,
and on page unload (via keepalive fetch). trackEvent never throws
and never blocks — failures are silently dropped with an optional
debug log.
vc.trackEvent('level_started', { level: 3 });identify(opts): Promise<void>
Promote an anonymous account to a claimed one.
await vc.identify({ email: 'player@example.com' });
// Magic-link email is sent; player clicks the link to confirm.
await vc.identify({ provider: 'google' });
// OAuth redirect is initiated; page reloads after return.Both paths cause a page reload on successful return. The SDK picks
the session up automatically on the next init() via its URL-token
detection.
Authentication model
Anonymous by default. The first time a player visits a page that
loads the SDK and calls init(), the SDK asks Supabase for an
anonymous session. The platform's DB creates a players row keyed to
the new auth user, with a generated display name like Player_a1b2c3.
Subsequent page loads reuse the stored session — anonymous players
retain their identity across visits on the same device.
Claiming an account. Calling identify({ email }) sends a magic
link; on click, the anonymous session is linked to the email-backed
identity. Calling identify({ provider: 'google' | 'apple' | 'discord' })
initiates an OAuth redirect. After either flow completes, the player
keeps their existing id, display name, score history, and
achievements — isAnonymous flips to false.
getPlayer() stays in sync. The cached player is updated on
init() and on Supabase auth state changes (token refresh, sign-out,
tab sync).
Error handling
Every failure the SDK throws is a VibecadeError. Catch it to inspect
the code field and surface something meaningful:
try {
await vc.submitScore({ score: 1234 });
} catch (err) {
if (err.code === 'SCORE_OUT_OF_RANGE') {
console.warn('score was rejected as implausible');
} else if (err.code === 'NETWORK_ERROR') {
console.warn('platform offline — will retry later');
} else {
console.error('score submission failed:', err.code, err.message);
}
}Error codes are exported as VibecadeErrorCode for type-safe comparison:
import { VibecadeErrorCode } from '@vibecade/sdk';
if (err.code === VibecadeErrorCode.ScoreOutOfRange) { ... }Always wrap SDK calls in try/catch. The SDK is additive — its
job is to enrich a game with platform features, not to be required
for the game to work. Platform outages, network failures, or rejected
calls must degrade gracefully, not crash gameplay.
Anti-cheat guidance
Two related knobs on a game's platform entry:
max_plausible_score is a ceiling. If set, submitScore rejects
submissions above it server-side. Set it to a value a human player
might plausibly reach; scores above probably mean a bug or a cheat.
requires_play_token makes the leaderboard competitive. When
true, submitScore rejects submissions that don't include a valid
playToken obtained from startPlay at the start of the session.
Tokens are one-use, server-issued, 10-minute-lived, and bound to the
player + game they were issued for.
A typical flow when play tokens are required:
await vc.init();
const { playToken } = await vc.startPlay();
// ... play happens ...
await vc.submitScore({ score: finalScore, playToken });Use requires_play_token on games with meaningful competitive
leaderboards. Skip it for casual games where the cost of an occasional
inflated score is low.
Nothing beyond these knobs is enforced in MVP — no replay validation, no statistical analysis, no ML. More sophisticated checks can layer on later; the SDK's anti-cheat surface is stable.
Behavior guarantees
- The SDK is optional. A web page that never loads the SDK is a perfectly valid consumer of the Vibecade platform. Games that integrate the SDK pick up platform features (identity, leaderboards, achievements, cloud saves, analytics). Games that don't, don't.
- The SDK never blocks gameplay on network. Every method is async;
callers are expected to
try/catch. A platform outage must not prevent a game from running. - Anonymous auth is the default. Players don't need to sign up before playing. Identity is persisted client-side. Claiming the account later keeps all existing data.
- Achievements are idempotent. Re-unlocking a one-shot achievement returns the original timestamp; progressive updates are monotonic and max-wins (regressions silently clamp to the stored value), so client-side dedup isn't needed.
- Play tokens auto-handled. Games with
requires_play_tokenget transparentstartPlay()+ retry onsubmitScorecalls that don't supply a token. Manualopts.playTokenstill works and skips the auto path. Detection caches after the first submit so subsequent submits pre-fetch directly. - Analytics is fire-and-forget.
trackEventnever throws; the SDK handles batching and retries best-effort internally. - Auto-toasts in launcher chrome. When the SDK runs inside a
Vibecade launcher iframe, successful
submitScoreandunlockAchievement(andsetAchievementProgresscalls that cross the target threshold) automatically post a message towindow.parentso the launcher can render a toast. Standalone games (window.parent === window) skip this step. No game-side work required to get launcher toast UX — the SDK handles it. PassautoHostMessages: falsein the config if your game already emits its ownwindow.parent.postMessagefor these events (pre-0.1.1 integrations); leaving both on produces duplicate toasts.
Compatibility
- Modern evergreen browsers (ES2020+).
- Any JavaScript environment that supports
fetchandlocalStorage. - Node.js is not a targeted runtime (the SDK assumes a browser-like
host with
window). For server-side use cases, use the Supabase client directly against the platform's Postgres.
License
Apache-2.0