@timeback/sdk
@timeback/sdk
TypeScript SDK for integrating Timeback into your application. Provides server-side route handlers and client-side components for activity tracking and SSO authentication.
Installation
bun add @timeback/sdk
# npm install @timeback/sdk
Quick Start
import { createTimeback } from '@timeback/sdk'
const timeback = await createTimeback({
env: 'staging',
api: { clientId: '...', clientSecret: '...' },
identity: { mode: 'custom', getEmail: async req => getSession(req)?.email },
})
Then: (1) create a server instance with your credentials, (2) mount the route handlers for your framework, (3) wrap your app with the client provider, (4) use hooks/composables to track activities.
Server Adapters
All server adapters use the same core configuration and require a timeback.config.json file:
// lib/timeback.ts
import { createTimeback } from '@timeback/sdk'
export const timeback = await createTimeback({
env: 'staging', // 'local' | 'staging' | 'production'
api: {
clientId: process.env.TIMEBACK_API_CLIENT_ID!,
clientSecret: process.env.TIMEBACK_API_CLIENT_SECRET!,
},
identity: {
mode: 'sso',
clientId: process.env.TIMEBACK_SSO_CLIENT_ID!,
clientSecret: process.env.TIMEBACK_SSO_CLIENT_SECRET!,
redirectUri: 'http://localhost:3000/api/auth/sso/callback/timeback',
onCallbackSuccess: async ({ user, state, redirect }) => {
// user.id is the timebackId (canonical stable identifier)
await setSession({ id: user.id, email: user.email })
return redirect(state?.returnTo ?? '/')
},
onCallbackError: ({ error, redirect }) => {
console.error('SSO Error:', error)
return redirect('/?error=sso_failed')
},
getUser: () => getSession(), // Return current user or undefined
},
})
Next.js
// app/api/timeback/[...timeback]/route.ts
import { toNextjsHandler } from '@timeback/sdk/nextjs'
import { timeback } from '@/lib/timeback'
export const { GET, POST } = toNextjsHandler(timeback)
Nuxt
// server/middleware/timeback.ts
import { nuxtHandler } from '@timeback/sdk/nuxt'
import { timeback } from '../lib/timeback'
export default defineEventHandler(async event => {
const response = await nuxtHandler({
timeback,
event,
})
if (response) return response
})
SvelteKit
// src/hooks.server.ts
import { building } from '$app/environment'
import { timeback } from '$lib/timeback'
import { svelteKitHandler } from '@timeback/sdk/svelte-kit'
import type { Handle } from '@sveltejs/kit'
export const handle: Handle = ({ event, resolve }) => {
return svelteKitHandler({
timeback,
event,
resolve,
building,
})
}
SolidStart
// src/middleware.ts
import { createMiddleware } from '@solidjs/start/middleware'
import { timeback } from '~/lib/timeback'
import { solidStartHandler } from '@timeback/sdk/solid-start'
export default createMiddleware({
onRequest: [
async event => {
const response = await solidStartHandler({
timeback,
event,
})
if (response) return response
},
],
})
TanStack Start
// src/routes/api/timeback/$.ts
import { createFileRoute } from '@tanstack/react-router'
import { toTanStackStartHandler } from '@timeback/sdk/tanstack-start'
import { timeback } from '@/lib/timeback'
const handlers = toTanStackStartHandler(timeback)
export const Route = createFileRoute('/api/timeback/
Express
// server.ts
import express from 'express'
import { toExpressMiddleware } from '@timeback/sdk/express'
import { timeback } from './lib/timeback'
const app = express()
app.use(express.json())
app.use('/api/timeback', toExpressMiddleware(timeback))
Client Adapters
React
// app/providers.tsx
'use client'
import { TimebackProvider } from '@timeback/sdk/react'
export function Providers({ children }: { children: React.ReactNode }) {
return <TimebackProvider>{children}</TimebackProvider>
}
// components/ActivityTracker.tsx
import { useEffect } from 'react'
import { SignInButton, useTimeback } from '@timeback/sdk/react'
function MyComponent() {
const timeback = useTimeback()
useEffect(() => {
if (!timeback) return
const activity = timeback.activity
.new({ id: 'lesson-1', name: 'Intro', course: { subject: 'Math', grade: 3 } })
.start()
return () => {
activity.end()
}
}, [timeback])
return <SignInButton size="lg" />
}
Profile Hook
Use useTimebackProfile to fetch user profile data. Supports manual and auto-fetch modes:
// Manual fetch
import { useTimebackProfile } from '@timeback/sdk/react'
function ProfileButton() {
const { state, canFetch, fetchProfile } = useTimebackProfile()
if (state.status === 'loaded') {
return <div>XP: {state.profile.xp}</div>
}
return (
<button onClick={fetchProfile} disabled={!canFetch}>
{state.status === 'loading' ? 'Loading...' : 'Load Profile'}
</button>
)
}
// Auto-fetch when verified
function AutoProfile() {
const { state } = useTimebackProfile({ auto: true })
if (state.status === 'loading') return <div>Loading...</div>
if (state.status === 'loaded') return <div>XP: {state.profile.xp}</div>
return null
}
Vue
Profile Composable
<script setup>
import { useTimebackProfile } from '@timeback/sdk/vue'
const { state, canFetch, fetchProfile } = useTimebackProfile()
</script>
<template>
<div v-if="state.status === 'loaded'">XP: {{ state.profile.xp }}</div>
<button v-else @click="fetchProfile" :disabled="!canFetch">
{{ state.status === 'loading' ? 'Loading...' : 'Load Profile' }}
</button>
</template>
<!-- app.vue -->
<script setup>
import { TimebackProvider } from '@timeback/sdk/vue'
</script>
<template>
<TimebackProvider>
<NuxtPage />
</TimebackProvider>
</template>
<!-- components/ActivityTracker.vue -->
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { SignInButton, useTimeback } from '@timeback/sdk/vue'
const timeback = useTimeback()
let activity
onMounted(() => {
if (timeback.value) {
activity = timeback.value.activity
.new({ id: 'lesson-1', name: 'Intro', course: { subject: 'Math', grade: 3 } })
.start()
}
})
onUnmounted(() => activity?.end())
</script>
<template>
<SignInButton size="lg" />
</template>
Svelte
<!-- +layout.svelte -->
<script>
import { initTimeback } from '@timeback/sdk/svelte'
initTimeback()
let { children } = $props()
</script>
{@render children()}
<!-- +page.svelte -->
<script>
import { onMount, onDestroy } from 'svelte'
import { SignInButton, timeback } from '@timeback/sdk/svelte'
let activity
onMount(() => {
if ($timeback) {
activity = $timeback.activity
.new({ id: 'lesson-1', name: 'Intro', course: { subject: 'Math', grade: 3 } })
.start()
}
})
onDestroy(() => activity?.end())
</script>
<SignInButton size="lg" />
Profile Store
<script>
import { timebackProfile, timebackProfileCanFetch, fetchTimebackProfile } from '@timeback/sdk/svelte'
</script>
<button onclick={fetchTimebackProfile} disabled={!$timebackProfileCanFetch}>
{$timebackProfile.status === 'loading' ? 'Loading...' : 'Load Profile'}
</button>
{#if $timebackProfile.status === 'loaded'}
<div>XP: {$timebackProfile.profile.xp}</div>
{/if}
Solid
// app.tsx
import { TimebackProvider } from '@timeback/sdk/solid'
export default function App() {
return (
<TimebackProvider>
<Router root={props => <Suspense>{props.children}</Suspense>}>
<FileRoutes />
</Router>
</TimebackProvider>
)
}
// components/ActivityTracker.tsx
import { onCleanup, onMount } from 'solid-js'
import { SignInButton, useTimeback } from '@timeback/sdk/solid'
function MyComponent() {
const timeback = useTimeback()
let activity
onMount(() => {
if (!timeback) return
activity = timeback.activity
.new({ id: 'lesson-1', name: 'Intro', course: { subject: 'Math', grade: 3 } })
.start()
})
onCleanup(() => activity?.end())
return <SignInButton size="lg" />
}
Profile Primitive
import { Show } from 'solid-js'
import { createTimebackProfile } from '@timeback/sdk/solid'
function ProfileButton() {
const { state, canFetch, fetchProfile } = createTimebackProfile()
return (
<Show
when={state.status === 'loaded'}
fallback={
<button onClick={fetchProfile} disabled={!canFetch}>
{state.status === 'loading' ? 'Loading...' : 'Load Profile'}
</button>
}
>
<div>XP: {state.profile.xp}</div>
</Show>
)
}
Identity Modes
SSO Mode
Uses Timeback as the identity provider via OIDC. When using createTimeback(), the SDK automatically resolves the Timeback user by email and returns an enriched TimebackAuthUser with user.id being the canonical timebackId (stable identifier).
identity: {
mode: 'sso',
clientId: process.env.TIMEBACK_SSO_CLIENT_ID!,
clientSecret: process.env.TIMEBACK_SSO_CLIENT_SECRET!,
redirectUri: 'http://localhost:3000/api/auth/sso/callback/timeback',
onCallbackSuccess: async ({ user, idp, state, redirect }) => {
// user.id is the timebackId (canonical stable identifier)
// user.email, user.name come from Timeback profile
// user.claims contains IdP data (sub, firstName, lastName, pictureUrl)
// idp.tokens and idp.userInfo contain raw OIDC data if needed
await setSession({ id: user.id, email: user.email })
return redirect(state?.returnTo ?? '/')
},
onCallbackError: ({ error, errorCode, redirect }) => {
// errorCode may be: missing_email, timeback_user_not_found,
// timeback_user_ambiguous, timeback_user_lookup_failed
console.error('SSO Error:', errorCode, error.message)
return redirect('/?error=sso_failed')
},
getUser: () => getCurrentSession(),
}
TimebackAuthUser Shape
The user in onCallbackSuccess is a TimebackAuthUser with this structure:
interface TimebackAuthUser {
id: string // Timeback user ID
email: string
name?: string
school?: { id: string; name: string }
grade?: number
claims: {
sub: string // OIDC subject identifier
email: string
firstName?: string
lastName?: string
pictureUrl?: string
}
}
Session Storage Guidance
For cookie-only sessions, store only the minimal payload to avoid cookie size limits:
// Recommended: store minimal session
await setSession({ id: user.id, email: user.email })
// Then in getUser, return what you stored:
getUser: req => {
const session = getSessionFromCookie(req)
return session ? { id: session.id, email: session.email } : undefined
}
For DB-backed sessions, you can store the full TimebackAuthUser if desired.
Custom Mode
For apps with existing auth (Clerk, Auth0, Supabase, etc.):
identity: {
mode: 'custom',
getEmail: async (req) => {
const session = await getSession(req)
return session?.email
},
}
The SDK resolves the Timeback user by email. You only need to provide the authenticated user's email address.
Identity-Only Integration
If you only need Timeback SSO authentication without activity tracking or Timeback API integration, use createTimebackIdentity(). This is a lightweight alternative that:
- Does not require Timeback API credentials
- Does not require
timeback.config.json
- Only exposes identity routes (sign-in, callback, sign-out)
- Returns raw OIDC user info (no Timeback profile enrichment)
Note: Unlike createTimeback(), the identity-only callback returns raw OIDC user info (sub, email, name, etc.) without resolving a Timeback user. Use createTimeback() if you need the canonical timebackId.
Edge Runtime Compatibility
The main @timeback/sdk entrypoint is edge-compatible and works in Cloudflare Workers,
Vercel Edge, Bun, Deno, and Node.js. No separate entrypoint is needed:
import { createTimebackIdentity, toNativeHandler } from '@timeback/sdk'
Note: createTimeback() (the full SDK) lazily loads shared config-reading
infrastructure at runtime. On edge runtimes without filesystem access,
createTimeback() will fail at call time.
Use createTimebackIdentity() for edge deployments.
Server Setup
// lib/timeback.ts
import { createTimebackIdentity } from '@timeback/sdk'
export const timeback = createTimebackIdentity({
env: 'production',
identity: {
mode: 'sso',
clientId: process.env.TIMEBACK_SSO_CLIENT_ID!,
clientSecret: process.env.TIMEBACK_SSO_CLIENT_SECRET!,
onCallbackSuccess: async ({ user, tokens, redirect }) => {
// user is raw OIDC userInfo (sub, email, name, picture, etc.)
// No Timeback profile enrichment in identity-only mode
await createSession({
sub: user.sub,
email: user.email,
name: user.name,
})
return redirect('/')
},
onCallbackError: ({ error, redirect }) => {
console.error('SSO Error:', error)
return redirect('/login?error=sso_failed')
},
getUser: req => getSessionFromRequest(req),
},
})
Next.js
// app/api/timeback/[...timeback]/route.ts
import { toNextjsHandler } from '@timeback/sdk/nextjs'
import { timeback } from '@/lib/timeback'
export const { GET, POST } = toNextjsHandler(timeback)
Express
// server.ts
import express from 'express'
import { toExpressMiddleware } from '@timeback/sdk/express'
import { timeback } from './lib/timeback'
const app = express()
app.use('/api/timeback', toExpressMiddleware(timeback))
All other framework adapters (Nuxt, SvelteKit, SolidStart, TanStack Start) work identically with identity-only instances.
User Profile
Use timeback.user.fetch() to retrieve the enriched Timeback profile for the
current user (identity + school/grade + courses + goals + XP):
const profile = await timeback.user.fetch()
console.log(profile.school?.name)
console.log(profile.xp?.today, profile.xp?.all)
profile.xp has the shape { today, all }, where:
today is computed over the current UTC day.
all is computed over a long-range analytics window (starting 2000-01-01).
Activity Tracking
Activities track time spent on learning content:
// Start an activity
const activity = timeback.activity
.new({
id: 'lesson-123',
name: 'Introduction to Fractions',
course: { subject: 'FastMath', grade: 3 },
})
.start()
// Pause/resume
activity.pause()
activity.resume()
// End with metrics
await activity.end({
totalQuestions: 10,
correctQuestions: 8,
xpEarned: 80,
masteredUnits: 1, // optional (default: omitted)
pctComplete: 67, // optional (default: omitted; clamped to 0–100 by the SDK server handler if present)
})
The SDK automatically sends activity data to your server, which forwards it to the Timeback API.
Note: Activity ingestion requires an effective Caliper sensor URL per course in timeback.config.json.
Sensor precedence (highest → lowest):
- per-course env override:
course.overrides[env].sensor
- per-course base override:
course.sensor
- top-level default:
config.sensor
{
"$schema": "https://timeback.dev/schema.json",
"name": "My App",
"sensor": "https://my-app.example.com/sensors/default",
"courses": [
{
"subject": "Math",
"grade": 3,
"courseCode": "MATH-3",
"sensor": "https://my-app.example.com/sensors/math",
"overrides": {
"staging": { "sensor": "https://staging.my-app.example.com/sensors/math" },
"production": { "sensor": "https://my-app.example.com/sensors/math" }
}
}
]
}
Advanced: Direct API Access
For advanced use cases that need to call Timeback services (OneRoster, Edubridge, Caliper, QTI) beyond what the SDK handlers provide, use timeback.api:
// Access via the timeback instance (lazy-initialized on first access)
const { data: users } = await timeback.api.oneroster.users.list({
limit: 10,
where: { role: 'student' },
})
// Access any Timeback service
const { data: orgs } = await timeback.api.oneroster.orgs.list()
await timeback.api.caliper.emit(caliperEvent)
Access Patterns
// lib/timeback.ts
import { createTimeback } from '@timeback/sdk'
export const timeback = await createTimeback({
env: 'staging',
api: {
clientId: process.env.TIMEBACK_API_CLIENT_ID!,
clientSecret: process.env.TIMEBACK_API_CLIENT_SECRET!,
},
identity: { mode: 'custom', getEmail: () => getSession()?.email },
})
// Access the API client via timeback.api
const { data: users } = await timeback.api.oneroster.users.list()
// Elsewhere in your app
import { timeback } from './lib/timeback'
// The client is available at timeback.api
const { data: orgs } = await timeback.api.oneroster.orgs.list()
Environment Mapping
The SDK's env config controls runtime mode, but for outbound API calls:
SDK env
API calls use
local
staging
staging
staging
production
production
This means env: 'local' uses staging Timeback services, so you can develop locally against real (staging) data without additional configuration.
When to Use
- Fetching data not exposed by SDK handlers (e.g., listing orgs, courses, enrollments)
- Emitting custom Caliper events
- Building admin dashboards or reporting tools
- Any direct Timeback API integration
Note: timeback.api is only available on the full SDK (createTimeback()), not on identity-only instances (createTimebackIdentity()).
)({
server: { handlers },
})
Express
__CODE_BLOCK_8__Client Adapters
React
__CODE_BLOCK_9__ __CODE_BLOCK_10__Profile Hook
Use __INLINE_CODE_1__ to fetch user profile data. Supports manual and auto-fetch modes:
__CODE_BLOCK_11__Vue
Profile Composable
__CODE_BLOCK_12__ __CODE_BLOCK_13__ __CODE_BLOCK_14__Svelte
__CODE_BLOCK_15__ __CODE_BLOCK_16__Profile Store
__CODE_BLOCK_17__Solid
__CODE_BLOCK_18__ __CODE_BLOCK_19__Profile Primitive
__CODE_BLOCK_20__Identity Modes
SSO Mode
Uses Timeback as the identity provider via OIDC. When using __INLINE_CODE_2__, the SDK automatically resolves the Timeback user by email and returns an enriched __INLINE_CODE_3__ with __INLINE_CODE_4__ being the canonical __INLINE_CODE_5__ (stable identifier).
__CODE_BLOCK_21__TimebackAuthUser Shape
The __INLINE_CODE_6__ in __INLINE_CODE_7__ is a __INLINE_CODE_8__ with this structure:
__CODE_BLOCK_22__Session Storage Guidance
For cookie-only sessions, store only the minimal payload to avoid cookie size limits:
__CODE_BLOCK_23__For DB-backed sessions, you can store the full __INLINE_CODE_9__ if desired.
Custom Mode
For apps with existing auth (Clerk, Auth0, Supabase, etc.):
__CODE_BLOCK_24__The SDK resolves the Timeback user by email. You only need to provide the authenticated user's email address.
Identity-Only Integration
If you only need Timeback SSO authentication without activity tracking or Timeback API integration, use __INLINE_CODE_10__. This is a lightweight alternative that:
- Does not require Timeback API credentials
- Does not require __INLINE_CODE_11__
- Only exposes identity routes (sign-in, callback, sign-out)
- Returns raw OIDC user info (no Timeback profile enrichment)
Note: Unlike __INLINE_CODE_12__, the identity-only callback returns raw OIDC user info (__INLINE_CODE_13__, __INLINE_CODE_14__, __INLINE_CODE_15__, etc.) without resolving a Timeback user. Use __INLINE_CODE_16__ if you need the canonical __INLINE_CODE_17__.
Edge Runtime Compatibility
The main __INLINE_CODE_18__ entrypoint is edge-compatible and works in Cloudflare Workers, Vercel Edge, Bun, Deno, and Node.js. No separate entrypoint is needed:
__CODE_BLOCK_25__Note: __INLINE_CODE_19__ (the full SDK) lazily loads shared config-reading infrastructure at runtime. On edge runtimes without filesystem access, __INLINE_CODE_20__ will fail at call time.
Use __INLINE_CODE_21__ for edge deployments.
Server Setup
__CODE_BLOCK_26__Next.js
__CODE_BLOCK_27__Express
__CODE_BLOCK_28__All other framework adapters (Nuxt, SvelteKit, SolidStart, TanStack Start) work identically with identity-only instances.
User Profile
Use __INLINE_CODE_22__ to retrieve the enriched Timeback profile for the current user (identity + school/grade + courses + goals + XP):
__CODE_BLOCK_29____INLINE_CODE_23__ has the shape __INLINE_CODE_24__, where:
- __INLINE_CODE_25__ is computed over the current UTC day.
- __INLINE_CODE_26__ is computed over a long-range analytics window (starting __INLINE_CODE_27__).
Activity Tracking
Activities track time spent on learning content:
__CODE_BLOCK_30__The SDK automatically sends activity data to your server, which forwards it to the Timeback API.
Note: Activity ingestion requires an effective Caliper sensor URL per course in __INLINE_CODE_28__.
Sensor precedence (highest → lowest):
- per-course env override: __INLINE_CODE_29__
- per-course base override: __INLINE_CODE_30__
- top-level default: __INLINE_CODE_31__
Advanced: Direct API Access
For advanced use cases that need to call Timeback services (OneRoster, Edubridge, Caliper, QTI) beyond what the SDK handlers provide, use __INLINE_CODE_32__:
__CODE_BLOCK_32__Access Patterns
__CODE_BLOCK_33__ __CODE_BLOCK_34__Environment Mapping
The SDK's __INLINE_CODE_33__ config controls runtime mode, but for outbound API calls:
| SDK __INLINE_CODE_34__ | API calls use |
|---|---|
| __INLINE_CODE_35__ | __INLINE_CODE_36__ |
| __INLINE_CODE_37__ | __INLINE_CODE_38__ |
| __INLINE_CODE_39__ | __INLINE_CODE_40__ |
This means __INLINE_CODE_41__ uses staging Timeback services, so you can develop locally against real (staging) data without additional configuration.
When to Use
- Fetching data not exposed by SDK handlers (e.g., listing orgs, courses, enrollments)
- Emitting custom Caliper events
- Building admin dashboards or reporting tools
- Any direct Timeback API integration
Note: __INLINE_CODE_42__ is only available on the full SDK (__INLINE_CODE_43__), not on identity-only instances (__INLINE_CODE_44__).