Guide: Testing, then going live


The sandbox

https://api.sandbox.playspace.health/v1

Self-serve. No agreement, no review, no call. Sign up, take a key, start building.

  • Three clinics, twelve clinicians, roughly two hundred synthetic clients on realistic caseloads — dense calendars, empty calendars, paediatric clients with caregivers, teenagers without.
  • Generation runs against the same models as production, capped at one hundred jobs per day. Results are real; they are just rate-limited.
  • Data resets every Sunday. Do not build anything that assumes an identifier survives the week.
  • No message ever leaves the sandbox. Emails and text messages are captured, not sent, and readable through playspace.messages.list() so you can assert on delivery without a mailbox.

Sandbox credentials are prefixed psc_sandbox_ and are rejected by production. Production credentials are rejected by the sandbox. There is no configuration in which sandbox traffic reaches real clinical data.


Unit tests: the mock

import { PlaySpaceMock } from '@playspace/sdk/testing'

const playspace = new PlaySpaceMock({ seed: 'deterministic' })

test('a completed session attaches a storybook to the chart', async () => {
  const job = await playspace.storybooks.generate({
    clinician: { externalId: 'staff_1' },
    subject: { externalId: 'client_1' },
    prompt: 'A story about a new school.',
  })
  const book = await job.wait()          // instant, from a fixture
  expect(book.status).toBe('ready')
})

The whole surface in memory, no network. Generation resolves against fixtures, so a suite is fast, deterministic and free.

Seed your own data and provoke failures without causing them:

const playspace = new PlaySpaceMock({
  fixtures: { patients: [{ externalId: 'client_55130', firstName: 'A.', lastName: 'R.' }] },
})

playspace.mock.failNext('storybooks.generate', new QuotaExceededError({ resource: 'storybooks' }))

Test the quota path. It is the failure your clinicians are most likely to meet and the one least likely to be exercised by accident.


Integration tests: the sandbox

Run the paths the mock cannot prove: real authentication, real rate limits, real generation, real frame rendering.

test('token mint rejects an unregistered origin', async () => {
  await expect(
    playspace.tokens.issue({
      session: sessionId,
      subject: { externalId: 'staff_1' },
      role: 'clinician',
      origins: ['https://not-registered.example.com'],
    })
  ).rejects.toThrow(ValidationError)
})

Browser tests

The surfaces render in a cross-origin frame, which shapes how you test them.

You cannot reach inside the frame. Do not try to select a figure in the sandtray from your test — the boundary is real and is the point. Assert on what crosses it: the frame mounted, the ready event fired with the expected role, your own chrome reacted.

test('the sandtray mounts for a clinician', async ({ page }) => {
  await page.goto('/appointments/appt_1')
  await page.getByRole('button', { name: 'Open sandtray' }).click()

  const frame = page.frameLocator('iframe[title="PlaySpace sandtray"]')
  await expect(frame.locator('body')).toBeVisible()

  await expect(page.getByTestId('playspace-ready')).toHaveAttribute('data-role', 'clinician')
})

Surface a test hook from your event handler so assertions have something stable to attach to:

<PlaySpaceProvider
  onEvent={(e) => {
    if (e.type === 'ready') setReadyRole(e.payload.role)
  }}
/>

Test both roles. Two browser contexts, two tokens, one session. The most common defect in a partner integration is a token route that mints clinician for everybody, and it is invisible until a client reports seeing the figure library.


Go-live checklist

Credentials

  • Production credentials issued and stored in your secret manager, not in an environment file in a repository.
  • Internet-protocol allowlisting configured on the production credential.
  • A rotation runbook exists. Two credentials can be live at once, so rotation is: issue, deploy, confirm traffic moved, revoke.
  • No PlaySpace secret appears in any browser bundle. Search your built assets for pss_ and confirm zero hits.

Origins

  • Every production origin registered, exactly — scheme, no trailing slash, no path.
  • Preview and staging origins registered if you render surfaces there.
  • frame-src https://embed.playspace.health present in your Content Security Policy.

The token route

  • Authorises the caller against your own session before minting. This is the entire security boundary.
  • Derives role from your record of who the clinician is, never from a client-supplied parameter.
  • Returns only the token value and expiry — never the whole mint response.
  • Rate-limited on your side. It is an unauthenticated-adjacent surface from an attacker's point of view.

Failure handling

  • onError wired to your error reporting, logging code and requestId.
  • token_refresh_failed alerts somebody — it means your token endpoint is down.
  • entitlement_denied and capability_missing show a clinician-readable message rather than a stack trace.
  • Quota warning at eighty percent, somewhere a practice administrator will see it.
  • QuotaExceededError schedules a retry at resetsAt rather than failing the operation.

Data handling

  • No client name or free-text search term in any URL your application constructs.
  • requestId logged on every failure; no client name in any support ticket.
  • Caregiver links carry canRequestExport set deliberately, not by default.
  • Export download links delivered through your authenticated channel, never emailed.

Operations

  • Change-feed cursor persisted durably. Losing it means replaying up to thirty days or missing everything since.
  • The change-feed worker is a singleton, or its handler is idempotent. Two workers on one cursor will double-process.
  • Session identifiers cached on your appointment records.
  • A monthly drift check on client counts.

Rolling out

Start with one clinic. Not one feature — one clinic, with every feature you intend to ship. Clinician feedback on a therapeutic surface is qualitative and arrives in conversation, not in metrics, and you want that conversation with ten clinicians before it is with a thousand.

Ship the linked pattern first if you are unsure. A hosted session link is one endpoint call and two links in your interface. It puts the product in front of clinicians in days rather than a quarter, and what you learn shapes the embedded integration you build afterwards.

Watch three things in the first month. Token mint failures by code, which catch integration defects. Quota consumption trend, which catches a pricing mismatch early. And the ratio of sessions created to sessions actually started, which catches an entry point nobody can find.


Getting help

Partner engineering: partner-engineering@playspace.health, or the shared channel opened when your sandbox key is issued.

Include the requestId and the failure is usually diagnosed the same day. Include a client's name and we will ask you to redact it and resend.