Play - Universal Frontend
Play is XMachines’ approach to building frontend applications that work identically across any JavaScript runtime—browser, Node, Deno, edge functions—with zero code changes.
The Problem
Most frontend frameworks tightly couple business logic to:
- Specific routing libraries
- Browser APIs
- Framework-specific state management
- View layer implementations
This makes it difficult to:
- Test business logic in isolation
- Reuse logic across platforms (web, mobile, desktop)
- Build truly universal apps (SSR + CSR + SSG with same code)
- Let AI agents control navigation based on business rules
Play’s Solution: Strict Separation
Play enforces a strict separation between three concerns:
graph LR
A[Actor<br/>Business Logic] -->|Signals| B[Runtime Adapter<br/>Infrastructure]
B -->|Events| A
A -->|Signals| C[View<br/>Presentation]
The Three Roles
1. The Actor (Business Logic Engine)
- Pure, environment-agnostic logic runtime
- Owns state, guards, errors, and route validity
- Emits Virtual Routes as derived intent
- Has zero knowledge of: Browser APIs, routing libraries, view frameworks
2. The Runtime Adapter (Infrastructure Layer)
- Environment-specific adapter (Browser, Native, Server, Test Runner)
- Reflects Actor output into the environment
- Forwards environment events to the Actor without interpretation
- Never enforces guards, validation, or business rules
3. The View (Passive Presentation)
- Passive consumer of Actor state
- No business rules or routing authority
- Renders based solely on Actor signals
Communication Medium: TC39 Signals
Play uses TC39 Signals exclusively for Actor ↔ Adapter ↔ View communication:
- Push-pull reactive model
- Synchronous, glitch-free propagation
- Standard, framework-agnostic API
Core Principles
1. Actor Authority
The Actor is the final authority on state and route validity.
If the browser says “navigate to /admin” but the Actor rejects it (user not authenticated), the Actor:
- Transitions to an error or fallback state (e.g.,
/login) - Emits the corrected Virtual Route
- Forces the browser to realign with Actor reality
2. Logic-Driven Navigation
Routes are derived from state, not configured separately:
states: { authenticated: { meta: { route: '/dashboard', // Virtual Route view: { type: 'Dashboard', props: { ... } } } }, unauthenticated: { meta: { route: '/login', view: { type: 'LoginForm', props: { ... } } } }}3. State-Driven Reset
Invalid external navigation is always overwritten by Actor-derived state.
Package Model
Play is distributed as multiple focused packages:
| Package | Purpose |
|---|---|
@xmachines/play-signals | TC39 Signals implementation |
@xmachines/play-ui | JSON schema + validation (Zod) |
@xmachines/play-actor | Abstract Actor interface |
@xmachines/play-xstate | XState v5 adapter + definePlayer |
@xmachines/play-router | Route tree protocol |
@xmachines/play-tanstack-router | TanStack Router adapter |
@xmachines/play-react | PlayRenderer component |
This allows you to:
- Swap state engines (XState, custom, etc.)
- Swap routers (TanStack, React Router, custom)
- Swap view layers (React, Vue, custom)
- Use only the pieces you need
Quick Start
Installation
npm install @xmachines/play-xstate @xmachines/play-react @xmachines/play-tanstack-router1. Define Your Player (Logic + Schema)
features/dashboard/player.ts - Pure logic, no React, no routing:
import { definePlayer } from '@xmachines/play-xstate';import { defineCatalog, z } from '@xmachines/play-ui';
// 1. Define the UI vocabulary (schema)export const catalog = defineCatalog({ components: { Dashboard: { props: z.object({ title: z.string() }) }, Metric: { props: z.object({ value: z.number(), label: z.string() }) } }});
// 2. Define the business logic (state machine)export const player = definePlayer({ catalog, machine: { initial: 'overview', states: { overview: { meta: { route: '/dashboard', view: { type: 'Dashboard', props: { title: 'Q4 Performance' }, children: [ { type: 'Metric', props: { value: 100, label: 'Sales' } } ] } } } } }});2. Create React Implementation
App.tsx - Bind logic to React and Router:
import { player, catalog } from './features/dashboard/player';import { PlayRenderer } from '@xmachines/play-react';import { createPlayRouter } from '@xmachines/play-tanstack-router';import { defineComponents } from '@json-render/react';import { RouterProvider } from '@tanstack/react-router';
// 1. Define React component implementationsconst components = defineComponents(catalog, { Dashboard: ({ props, children }) => ( <div className="dashboard"> <h1>{props.title}</h1> {children} </div> ), Metric: ({ props }) => ( <span className="metric"> {props.label}: {props.value} </span> )});
// 2. Create router with type-safe bindingsconst router = createPlayRouter({ player, components, // Type-checked against catalog component: PlayRenderer});
// 3. Renderexport function App() { return <RouterProvider router={router} />;}That’s it! Your business logic is completely decoupled from React and routing.
Use Cases
Universal Rendering
Same business logic works for:
- SSR (server-side rendering)
- CSR (client-side rendering)
- SSG (static site generation)
- Hybrid (islands architecture)
Multi-Platform
- Web (browser)
- Mobile (React Native)
- Desktop (Electron)
- Edge functions (Cloudflare Workers, Vercel Edge)
AI-Driven Navigation
Let AI agents control navigation by sending events to the Actor. The Actor’s business rules determine valid transitions.
Testing
Test business logic in isolation—no DOM, no routing library, just pure state machine logic:
import { createActor } from 'xstate';import { player } from './player';
test('navigation requires auth', () => { const actor = player.create({ /* ... */ }); actor.start();
// Attempt to navigate without auth actor.send({ type: 'NAVIGATE', path: '/dashboard' });
// Actor rejects and redirects to login expect(actor.currentRoute.value).toBe('/login');});Architecture Deep Dive
For complete technical specifications, see the Play RFC.
Key Topics Covered in RFC:
- Detailed architecture roles and responsibilities
- Signal-based reactive substrate
- Virtual Route protocol
- Package breakdown and interfaces
- Type safety guarantees
- Lock statements (invariants)
Learn More
- RFC: Play - Complete technical specification
- Guides - Understand XMachines fundamentals
- Platform Overview - Other platform options
Status
Play is currently in RFC/design phase. See the Play RFC for design specifications. Package implementations are in development.