PlaycademyPlaycademy

Server

Server-side SDK for backend routes and API endpoints

Overview

The server SDK (@playcademy/sdk/server) provides server-side APIs for integrating Playcademy into your own backend infrastructure (Express, Next.js, custom Node.js servers, etc.).

Not for Playcademy Custom Routes

If you're using Playcademy's custom routes, these features are already integrated.

This SDK is for developers building their own external backends.

PlaycademyClient

Initialization

Initialize the client with your API key:

import { PlaycademyClient } from '@playcademy/sdk/server'

const client = await PlaycademyClient.init({
    apiKey: process.env.PLAYCADEMY_API_KEY,
    gameId: 'my-project-id',
})

Configuration:

OptionTypeDescription
apiKeystringPlaycademy API key (required)
gameIdstringProject identifier (required)
configobjectConfig object (optional, skips file loading if provided)
configPathstringPath to playcademy.config.js (optional, auto-discovered)
baseUrlstringAPI base URL (defaults to production)

Config Loading

playcademy.config.js is auto-discovered if config or configPath are not provided

For environments without filesystem access, pass the config directly:

import { PlaycademyClient } from '@playcademy/sdk/server'

import type { PlaycademyConfig } from '@playcademy/sdk/server'

const config: PlaycademyConfig = {
    name: 'My Project',
    integrations: {
        timeback: {
            course: {
                subjects: ['Math'],
                grades: [3, 4, 5],
                title: 'Elementary Math',
            },
        },
    },
}

const client = await PlaycademyClient.init({
    apiKey: process.env.PLAYCADEMY_API_KEY!,
    gameId: 'my-project',
    config,
})

Client Structure

The server client provides a focused API for backend operations:

const client = await PlaycademyClient.init({ ... })

// Namespaces
client.timeback.*     // Timeback integration methods
client.gameId         // Project identifier property
client.config         // Loaded configuration object

Current Namespaces:

NamespacePurpose
timebackEnd learning activities and submit to Timeback

More Coming Soon

Additional namespaces for project management, user operations, and platform integrations will be added in future releases.

Timeback Methods

client.timeback.endActivity

End a learning activity and submit results to Timeback:

// Minimal example
await client.timeback.endActivity('student-123', {
    activityData: {
        activityId: 'math-quiz-1',
    },
    scoreData: {
        correctQuestions: 8,
        totalQuestions: 10,
    },
    timingData: {
        durationSeconds: 300, // 5 minutes
    },
})

// With optional overrides
await client.timeback.endActivity('student-123', {
    activityData: {
        activityId: 'math-quiz-1',
        activityName: 'Basic Arithmetic Quiz', // Optional
    },
    scoreData: {
        correctQuestions: 8,
        totalQuestions: 10,
    },
    timingData: {
        durationSeconds: 300,
    },
    xpEarned: 15, // Optional: override automatic XP calculation
})

Parameters:

ParameterTypeRequiredDescription
studentIdstringYesStudent identifier (Timeback ID)
payload.activityData.activityIdstringYesUnique activity identifier
payload.activityData.activityNamestringNoHuman-readable name (auto-derived from ID if empty)
payload.scoreData.correctQuestionsnumberYesNumber of correct answers
payload.scoreData.totalQuestionsnumberYesTotal number of questions
payload.timingData.durationSecondsnumberYesActivity duration in seconds
payload.xpEarnednumberNoXP override (bypasses automatic calculation)

Auto-filled metadata:

  • activityName: Prettified from activityId ("math-quiz-1" → "Math Quiz 1") if not provided
  • subject, appName, courseName, sensorUrl: From your app config

client.timeback.courseId

Access the Timeback course ID:

const courseId = client.timeback.courseId // string | undefined

When Is This Set?

The course ID is automatically fetched from the platform API the first time you call endActivity().

Before that, this property is undefined.

Client Properties

client.gameId

const gameId = client.gameId

client.config

const config = client.config
console.log(config.name)
console.log(config.integrations?.timeback)

verifyGameToken

Verify Playcademy tokens to authenticate users.

import { verifyGameToken } from '@playcademy/sdk/server'

// Extract token from Authorization header
const token = request.headers.get('Authorization')?.split(' ')[1]
const { user, gameId, claims } = await verifyGameToken(token)

// User is authenticated
return new Response(JSON.stringify({ userId: user.sub }), {
    headers: { 'Content-Type': 'application/json' },
})

Parameters:

ParameterTypeDescription
tokenstringToken to verify
options.baseUrlstringAPI base URL (auto-detected from env)

Returns:

{
    claims: Record<string, unknown>  // JWT claims
    gameId: string                   // Game ID
    user: {
        sub: string                  // User ID
        email: string                // Email address
        name: string                 // Display name
        email_verified: boolean      // Email verification status
        given_name?: string          // First name (optional)
        family_name?: string         // Last name (optional)
        timeback_id?: string         // Timeback student ID (optional)
        [key: string]: unknown       // Additional attributes
    }
}

Environment Variables:

The function checks these in order:

  1. options.baseUrl (if provided)
  2. PLAYCADEMY_BASE_URL
  3. PUBLIC_PLAYCADEMY_BASE_URL
  4. NEXT_PUBLIC_PLAYCADEMY_BASE_URL

Usage Examples

Express.js

import express from 'express'

import { verifyGameToken } from '@playcademy/sdk/server'

const app = express()

app.get('/api/user', async (req, res) => {
    const token = req.headers.authorization?.split(' ')[1]
    const { user } = await verifyGameToken(token)

    res.json({ message: `Hello, ${user.email}!` })
})

Next.js API Route

app/api/user/route.ts
import { NextRequest, NextResponse } from 'next/server'

import { verifyGameToken } from '@playcademy/sdk/server'

export async function GET(request: NextRequest) {
    const token = request.headers.get('Authorization')?.split(' ')[1]
    const { user } = await verifyGameToken(token)

    return NextResponse.json({ message: `Hello, ${user.email}!` })
}

Timeback Integration

import { PlaycademyClient, verifyGameToken } from '@playcademy/sdk/server'

// Initialize client once
const client = await PlaycademyClient.init({
    apiKey: process.env.PLAYCADEMY_API_KEY!,
    gameId: 'my-game',
})

// In your route handler
const token = request.headers.get('Authorization')?.split(' ')[1]
const { user } = await verifyGameToken(token)

// Parse request body
const body = await request.json()
const { activityData, scoreData, timingData, xpEarned } = body

// End activity and submit to Timeback
await client.timeback.endActivity(user.timeback_id, {
    activityData,
    scoreData,
    timingData,
    xpEarned,
})

return new Response(JSON.stringify({ success: true }), {
    headers: { 'Content-Type': 'application/json' },
})

Environment Setup

Set your API key in .env:

PLAYCADEMY_API_KEY=your-api-key-here
PLAYCADEMY_BASE_URL=https://hub.playcademy.net

Getting Your API Key

Your API key is displayed once after running playcademy login

See CLI Authentication for details


What's Next?

On this page