Browser
Client-side SDK for browser environments
Overview
The browser SDK provides namespaces for interacting with the Playcademy platform from your project frontend.
All methods are accessed through the PlaycademyClient instance.
import { PlaycademyClient } from '@playcademy/sdk'
const client = await PlaycademyClient.init()
const user = await client.users.me() // get current user
await client.scores.submit(1500, { level: 5 }) // submit scoreInitialization
PlaycademyClient.init() automatically detects your environment and configures the client accordingly.
See SDK Initialization for details.
Core Namespaces
client.users
Retrieve current user information.
// Get current user
const user = await client.users.me()client.scores
Submit scores for your project.
// Submit a score
const result = await client.scores.submit(1500, {
level: 5,
difficulty: 'hard',
perfectRun: true,
})client.identity
Connect external identity providers (Google, Discord, etc.) to user accounts.
// Connect Google account
const result = await client.identity.connect({
provider: 'google',
callbackUrl: '/auth/callback',
})
if (result.success) {
console.log('Connected:', result.user)
}Integration Namespaces
client.timeback
Track learning activities with automatic XP calculation. Access user context, XP, and mastery progress for content gating.
user
Access the user's Timeback context via client.timeback.user:
const id = client.timeback.user.id // User's Timeback ID
const role = client.timeback.user.role // 'student' | 'parent' | 'teacher' | ...
const enrollments = client.timeback.user.enrollments // App-scoped course enrollments (with enrollment id)
const orgs = client.timeback.user.organizations // App-scoped organizations
// Fetch fresh data from server (cached for 5 min)
const fresh = await client.timeback.user.fetch()
const forced = await client.timeback.user.fetch({ force: true })
// Refresh only enrollments from server (cached for 5 min)
const freshUser = await client.timeback.user.refresh({ only: ['enrollments'] })
// XP and mastery (cached for 5s)
const xp = await client.timeback.user.xp.fetch()
const mastery = await client.timeback.user.mastery.fetch()
const hgm = await client.timeback.user.highestGradeMastered.fetch({ subject: 'Math' })
if (hgm.highestGradeMastered === null) {
// No mastered grade has been established for this subject yet.
}Refresh & Fetch
Enrollment refresh uses the same 5-minute cache as user.fetch(). Pass { force: true } to bypass it.
See Timeback Integration → User Context for full xp, mastery, and highestGradeMastered options, including the highestGradeMastered: null response when no mastered grade exists yet.
App-Scoped
Enrollments and organizations are filtered to courses defined in your playcademy.config.js.
startActivity
Start tracking an activity. Only activityId is required:
// Minimal (most common)
const { runId: generatedRunId } = client.timeback.startActivity({
activityId: 'math-quiz-1', // automatically derived to "Math Quiz 1"
})
// With custom name override
client.timeback.startActivity({
activityId: 'math-quiz-1',
activityName: 'Advanced Multiplication Quiz',
})
// Resumable activity - reuse a stable runId across sessions
const activityId = 'math-quiz-1'
const runId = localStorage.getItem(`run:${activityId}`) ?? crypto.randomUUID()
localStorage.setItem(`run:${activityId}`, runId)
client.timeback.startActivity({ activityId }, { runId })startActivity() returns { runId }, using your supplied UUID when provided or a new SDK-generated UUID otherwise.
During an active activity, the same value is available as client.timeback.currentRunId; it is undefined when no activity is active.
Auto-filled Metadata
The SDK automatically fills in metadata from your project config:
- activityName: Derived from activityId ("math-quiz-1" → "Math Quiz 1")
- appName, subject, sensorUrl: From
playcademy.config.{js,json}
You can override any of these by providing them explicitly.
Automatic Tracking
Once started, the SDK flushes heartbeats every 15 seconds, auto-pauses the timer when the tab is hidden, and marks the activity inactive after long visible-idle stretches.
Tune it via the optional second argument (pausedHeartbeatTimeoutMs, heartbeatIntervalMs, inactivityTimeoutMs, runId).
See Timeback Integration → Activity Tracking for more details.
endActivity
End the current activity and submit results:
// Auto-calculate XP based on score
await client.timeback.endActivity({
correctQuestions: 8,
totalQuestions: 10,
})
// Override XP calculation
await client.timeback.endActivity({
correctQuestions: 8,
totalQuestions: 10,
xpAwarded: 15, // award exactly 15 XP
})
// Report mastery progress
await client.timeback.endActivity({
correctQuestions: 8,
totalQuestions: 10,
masteredUnits: 1, // incremental
// or: masteredUnitsAbsolute: 7 (absolute total, platform computes delta)
})
// Attach custom Caliper extensions
await client.timeback.endActivity({
correctQuestions: 3,
totalQuestions: 10,
extensions: {
reason: 'below-threshold',
threshold: 8,
},
})course.advance
Move the current student to the next configured Timeback course after mastery is complete:
const result = await client.timeback.course.advance()
if (result.promotion.status === 'promoted') {
console.log('Advanced to:', result.promotion.nextCourseId)
}
if (result.promotion.status === 'no-next-course') {
console.log('Student completed the final configured course')
}
if (result.promotion.status === 'not-mastered') {
console.log(
`Keep practicing: ${result.promotion.masteredUnits}/${result.promotion.masterableUnits} units mastered`,
)
}
// For games with multiple subject ladders
await client.timeback.course.advance({ subject: 'Math' })no-next-course and not-mastered are returned as promotion results. Validation and configuration issues, such as unavailable mastery analytics or ambiguous multi-subject enrollment, reject as SDK ApiErrors.
course.unenroll
Unenroll the current student from their active Timeback course after mastery is complete:
const result = await client.timeback.course.unenroll()
if (result.unenrollment.status === 'unenrolled') {
console.log('Unenrolled from:', result.unenrollment.currentCourseId)
}
if (result.unenrollment.status === 'not-mastered') {
console.log('Student must finish mastery before unenrolling')
}
// Explicitly allow an early exit
await client.timeback.course.unenroll({ subject: 'Math', force: true })Forced early exits are logged by the platform for operational review.
{pause,resume}Activity
Pause the timer during instructional moments and resume when ready:
client.timeback.startActivity({ activityId: 'math-quiz-1' })
// Student attempts problems...
if (studentAnswerWrong) {
// Pause timer to show correct answer
client.timeback.pauseActivity()
// Let student learn from mistake
showCorrectAnswer()
// Resume when they continue playing
client.timeback.resumeActivity()
}
// End activity (only active problem-solving time counted)
await client.timeback.endActivity({
correctQuestions: 8,
totalQuestions: 10,
})See Timeback Integration for complete documentation.
client.backend
Call your backend API routes.
These methods connect to the server-side routes you create in your server/api/ directory.
Learn more here.
Always Use sdk.backend
Don't use plain fetch() for your backend routes.
The SDK automatically includes the platform authentication token, which is required for c.get('playcademyUser') to work in your routes.
// ✅ Correct - includes platform token
const data = await client.backend.get('/my-route')
// ❌ Wrong - playcademyUser will be null
const data = await fetch('/api/my-route').then(r => r.json())get
Make GET requests:
const data = await client.backend.get('/hello')
console.log(data.message)
// With custom headers
const data = await client.backend.get('/protected', {
'X-Custom-Header': 'value',
})post / put / patch
Make requests with body data:
// POST request
const result = await client.backend.post('/validate', {
answer: 'paris',
})
// PUT request
await client.backend.put('/settings', {
volume: 0.8,
})
// PATCH request
await client.backend.patch('/profile', {
displayName: 'NewName',
})delete
Delete resources:
await client.backend.delete('/cache/clear')download
Download binary files:
const response = await client.backend.download('/files?key=report.pdf')
const blob = await response.blob()
// Trigger browser download
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'report.pdf'
a.click()
URL.revokeObjectURL(url)Downloading Files
The download method returns a raw Response object, allowing you to access the blob, streams,
or other binary data.
url
Build URLs for HTML elements (images, videos, etc.):
// Tagged template literal (recommended)
const imageUrl = client.backend.url`/assets/${sprite.key}`
<img src={imageUrl} />
// Regular function call
const videoUrl = client.backend.url('/videos/intro.mp4')
<video src={videoUrl} />Perfect for catchall routes that serve files by path.
See Custom Routes and Bucket Storage for more examples.
Runtime & Lifecycle
client.runtime
Game lifecycle events, messaging, and static asset loading.
// Signal ready
client.runtime.ready()
// Exit
client.runtime.exit()
// Listen for pause
client.runtime.onPause(() => {
pauseGame()
})
// Listen for resume
client.runtime.onResume(() => {
resumeGame()
})
// Load static assets at runtime
const levelData = await client.runtime.assets.json`levels/level-${id}.json`
img.src = client.runtime.assets.url`badges/${badgeType}.png`
audio.src = client.runtime.assets.url`sfx/${soundEffect}.wav`client.demo
Demo-mode identity and lifecycle helpers.
Always present on the client, but each method throws when client.mode is not 'demo'. Gate your demo-specific UI paths on the mode check and call freely inside that branch.
Use it to manage the anonymous demo player's display name and tell the host landing-page shell when the demo has ended.
if (client.mode === 'demo') {
const profile = await client.demo.profile.get()
if (profile.isDefault) {
await client.demo.profile.update({ displayName: 'linus-torvalds' })
}
client.demo.end(1500, {
durationMs: 60_000,
metadata: { level: 3, outcome: 'won' },
})
}| Method / Argument | Type | Required | Description |
|---|---|---|---|
profile.get() | Promise<{ displayName: string; isDefault: boolean }> | — | Read the anonymous demo player's profile. isDefault tells you if the handle is still the placeholder. |
profile.update(updates) | Promise<{ displayName: string; isDefault: boolean }> | ✅ | Update the demo profile (today: required { displayName }) and get the updated row back |
end(score, options?) | void | ✅ | Signal the end of the demo round to the landing shell. score is required; options (below) are extras |
options.durationMs | number | — | Active demo time in milliseconds |
options.metadata | Record<string, unknown> | — | Any additional data the shell needs |
See also
See Launch Modes to learn more about the different launch modes and what is available in each mode.
Event System
Listen for platform events:
// Pause event
client.on('pause', () => {
pause()
})
// Resume event
client.on('resume', () => {
resume()
})
// Exit requested
client.on('exit', () => {
cleanup()
})