StoryletEngine Client for JavaScript
Storylet Studio publishes .storyworld bundles - precompiled storylet data ready to play at runtime. Most JS-based games / apps load those bundles in-process via the StoryletEngine for JavaScript package. StoryletEngine Client for JavaScript is the other JS distribution: a REST + SSE adapter that drives a server-hosted engine instead. The engine itself runs on the StoryletEngine Server, and your code talks to it over the network using the same call patterns it would use against the local engine.
This page orients you: what the client gives you, when to reach for it instead of the local engine, and what's involved in shipping a Storylet Studio storyworld through a server-driven JS host. The distribution ships with full installation and API docs of its own, so treat this as the overview.
When you'd use this instead of the local plugin
The local JS plugin is right for most JS-based games - the engine runs in-process inside your code, no server involved. Reach for this package when one of the following is true:
- Several devices at the same site need to share one session. A kiosk plus IoT controllers plus a projection display, all reading from and writing to the same playthrough. The local engine is single-process, so the server holds the authoritative session and every device joins it.
- The storyworld lives on the server. Publish once, and every client plays the live version automatically. Rollback / promote happens server-side. Saves persist across player devices, whichever one wrote them.
- A game backend hosts the engine. Thin clients (web pages, mobile apps, Twitch overlays) hit the backend over HTTP rather than embedding the engine themselves.
If none of those describe your game, use the local plugin - it's simpler and keeps the server out of the loop entirely.
What's a storylet?
New to the model? Here's a one-paragraph primer (the Concepts page has the longer version):
A storylet is a small, self-contained narrative beat: a title, a description, optional conditions on game state, and one or more outcomes that change game state when chosen. A storyworld is the published bundle of every storylet, plus the sites (places the player visits), zones (groups of sites), and authored properties. A session is one playthrough; calling prime deals storylets into each site's slots, getSiteHand(siteGameId) reads what's currently dealt at a site, and play(outcome) applies a chosen outcome's state changes. Outcomes can be gated by a condition: each DrawnOutcome carries an available flag and play() is rejected for an unavailable outcome - both flow through the remote client unchanged.
In the server-client model, "session" picks up two extra ideas: an app (one deployment site - one venue, one street game, one cloud backend), and a role (the joined device's permission level: player, controller, or spectator).
What you need
- A modern JavaScript runtime - any of: a browser (Chrome / Firefox / Safari current), Node
>=20, Bun, Deno, Cloudflare Workers, Electron, Tauri. The host needs a globalfetchandEventSource; older Node versions can pass polyfills. - A running StoryletEngine Server with an app registered for your storyworld. The app's
sk_app_*API key is what your trusted device (kiosk / backend / IoT controller) uses to create sessions. - The StoryletEngine Client JavaScript package. Unlike the other plugins, this one isn't on the public downloads site - it's an internal distribution. Contact the Storylet Studio team via storylet.studio to get the package zip.
What the distribution gives you
- Client under
client/. Multi-format build: ESM (dist/index.js) for modern bundlers (Vite, webpack, esbuild, Next.js), CJS (dist/index.cjs) for older Node / Jest without ESM config, and IIFE / browser-global (dist/index.umd.jsand minified.umd.min.js) for<script src=…>users with no build step. - TypeScript definitions (
dist/index.d.ts) for IntelliSense in any JS / TS editor. - Source maps alongside every emitted file for stack-trace fidelity in dev.
createRemoteSession+joinRemoteSessionfactories. One for the trusted device that owns the session lifecycle (kiosk / backend / IoT controller, using ansk_app_*key); one for joining devices (phones / web frontends scanning a QR code, using a session-scopedsj_*JWT).RemoteSessioninterface that mirrors the local engine'sRuntimeSessionshape - same method names, same argument conventions, same drawn-outcomeplay()overload - with every method wrapped inPromise<...>. Engine types are re-exported (DrawnStorylet,DrawnOutcome, etc.) so values flow between local and remote sessions without conversion.- SSE-backed subscriptions.
session.subscribe(listener)andsession.subscribeKey(key, listener)work just like the local engine. The client opens a server-sent-events connection in the background and dispatchesproperty/log/site-hand/device/session.idle/session.ended/replay-lostevents. Auto-reconnect via the host'sEventSource. - Save / restore via
.storyworldstate. The same portable file format the Unity / Unreal players and the authoring tool's Simulate view read and write. A save produced on the server loads into a local session, and the other way round. - Zero runtime dependencies. The package depends on
@storylets/engineonly for type definitions; nothing in the runtime client imports engine code. It works in any JS environment withfetch+EventSourceavailable.
Install routes
Two options:
- Bundler project (Vite / webpack / Next.js / Remix / etc.). Unzip the
StoryletEngineClient/folder into your project'svendor/(or wherever you keep third-party deps), then add"@storylets/engine-client": "file:./vendor/StoryletEngineClient/client"to yourpackage.json#dependencies.npm installresolves the file: path, andimport { createRemoteSession } from "@storylets/engine-client"works. - Vanilla browser script. Drop
client/dist/index.umd.min.jssomewhere your HTML can reach and reference it with a<script>tag. The IIFE exposes a singleStoryletEngineClientglobal. No build step, nonode_modules.
Quickstart (3 minutes)
The full quickstart with auth model + multi-device join lives in the package's own README. The short form:
- Pick an install route from above.
- Open a session. The trusted device (kiosk / backend) creates one with
const session = await createRemoteSession({ baseUrl, appKey }). The server returns a session id and a session-scoped JWT (sj_*) that the client uses for every subsequent call. Joining devices attach viaawait joinRemoteSession({ baseUrl, joinToken, sessionId }). - Prime the slots.
await session.primeAllSlots()fills every site with storylets the engine reckons are eligible right now. Without it, sites are empty. Re-prime after everyplay()oradvanceTurn(). - Read a hand at a site.
const hand = await session.getSiteHand("site_inn01")returns the storylets currently dealt there - what the player can choose to do at that location. - Play an outcome. When the player picks one,
await session.play(outcome)applies it on the server. Then re-prime so evictions and new conditions resolve. Other joined devices see the change via SSE. - Subscribe to updates.
session.subscribe((event) => { /* re-render */ })fires whenever any device's action changes server-held state. Listener registration is sync; the SSE connection underneath is automatic.
Moving from local to remote is small in shape - "add await to every session call site" - and zero in surface area: the client re-exports the same DrawnStorylet / DrawnOutcome / ActionLogEntry types, so a game's UI rendering code doesn't change.
Auth model in one paragraph
The server uses three Bearer-token tiers (full detail in Publishing to the StoryletEngine Server). Game clients only deal with two:
sk_app_<random>- long-lived application key. Held by every trusted device at one deployment site (kiosk + IoT controllers + game backend). It lets the device create sessions and drive gameplay. Pass it tocreateRemoteSession({ appKey }).sj_<jwt>- short-lived session-scoped JWT. Joining devices receive one (usually by scanning a QR code the kiosk displays). Pass it tojoinRemoteSession({ joinToken, sessionId }). The token carries the device's role (player/controller/spectator), which the server enforces on every call.
The third tier (sk_org_* org admin keys) is for the management UI on server.storylet.studio - game clients never hold one.
The dev-time inspector
In the v1 release, the @storylets/inspector debug panel (the floating State / Properties / Log pill the local plugin and the deployed players use) is not yet wired up to remote sessions. It needs the storyworld's site / storylet catalogue to render its tabs in the gameIds you recognise, and the server doesn't expose that to clients yet. Inspector integration is on the roadmap for a Phase 2 release - either by extending the session-create response to include the bundle metadata, or by adding a dedicated catalogue endpoint. The package CHANGELOG flags this explicitly.
Cross-runtime parity
Same .storyworld bundle format as every other port. A storyworld authored in Storylet Studio runs the same way against:
- the local
@storylets/enginepackage, - the StoryletEngine Server (via this client),
- StoryletEngine for Unity,
- StoryletEngine for Unreal.
Saves are portable across all four via the .storyworldstate file envelope.
Where to next
- The distribution's own
README.md- install routes, auth model, full API surface, the multi-device join walkthrough. - StoryletEngine for JavaScript - the local in-process plugin. Use that one if your game runs the engine itself and doesn't need a server in the loop.
- Publishing to the StoryletEngine Server - author flow: stage a bundle from the Publish page so it appears in the server's management UI.
- Monitoring the StoryletEngine Server - operator flow: the Applications list, the Session Inspector, promote / rollback / retire, save / restore world state.
- Concepts - storylet, site, zone, act, slot, hand, outcome - the model in detail.
- Game Data - the per-storylet custom fields a player shell typically reads to drive presentation (UI, dialogue, audio).