Introduction
Air Jam is a platform for building "AirConsole-style" multiplayer games where a computer/TV acts as the host display and smartphones become game controllers. The platform enables developers to create interactive games with minimal setup while providing players with an intuitive, scan-and-play experience.
Key Features
- Zero App Download: Players join by scanning a QR code—no app store required
- Instant Multiplayer: Seamlessly connect up to 8 smartphones as controllers
- Developer Friendly: Built with modern web technologies (React, TypeScript)
- Type Safe: End-to-end type safety with Zod schema validation
- Performance Optimized: Tap-safe button input with fixed tick cadence
- Haptic Feedback: Send vibration patterns to controllers for game events
How It Works
Quick Start
1. Install the SDK
pnpm add @air-jam/sdk2. Wrap Your App
import { airjam } from "./airjam.config";
export const App = () => (
<Routes>
<Route
path="/"
element={
<airjam.Host
onPlayerJoin={(player) => {
console.log(`${player.label} joined!`);
}}
onPlayerLeave={(id) => {
console.log(`Player ${id} left`);
}}
>
<HostView />
</airjam.Host>
}
/>
<Route
path={airjam.paths.controller}
element={
<airjam.Controller>
<ControllerView />
</airjam.Controller>
}
/>
</Routes>
);import { createAirJamApp, env } from "@air-jam/sdk";
import { z } from "zod";
const gameInputSchema = z.object({
direction: z.number().min(-1).max(1),
action: z.boolean(),
});
export const airjam = createAirJamApp({
runtime: env.vite(import.meta.env),
controllerPath: "/controller",
input: { schema: gameInputSchema },
});3. Create Your Host View
The path below matches the current starter template layout. It is a good default, not a required framework filename.
import { useAirJamHost } from "@air-jam/sdk";
const HostView = () => {
const host = useAirJamHost();
// In your game loop (e.g., useFrame or requestAnimationFrame):
// host.players.forEach(p => {
// const input = host.getInput(p.id);
// if (input) {
// player.y += input.direction * SPEED;
// if (input.action) fireLaser(p.id);
// }
// });
return (
<div className="relative min-h-screen bg-black text-white">
<header className="absolute top-0 right-0 left-0 z-10 flex items-center justify-between border-b border-white/10 bg-black/70 px-4 py-2 backdrop-blur">
<span>Room {host.roomId}</span>
<button
onClick={
host.runtimeState === "playing"
? host.pauseRuntime
: host.resumeRuntime
}
>
{host.runtimeState === "playing" ? "Pause" : "Resume"}
</button>
</header>
<GameCanvas />
</div>
);
};Mount host ownership once at <airjam.Host ...>, then use useAirJamHost() only as a consumer hook inside that boundary.
4. Create Your Controller View
The path below also matches the current starter template layout. If your project uses a different file shape, keep the same ownership boundary even if the filename changes.
import {
useAirJamController,
useControllerTick,
useInputWriter,
} from "@air-jam/sdk";
import { useRef } from "react";
const ControllerView = () => {
const controller = useAirJamController();
const writeInput = useInputWriter();
const directionRef = useRef(0);
useControllerTick(
() => {
writeInput({
direction: directionRef.current,
action: false,
});
},
{
enabled:
controller.connectionStatus === "connected" &&
controller.runtimeState === "playing",
intervalMs: 16,
},
);
};Canonical Architecture: Three Lanes
Use one lane per concern:
- Input lane:
useControllerTick+useInputWriter(controller),getInput/useGetInput(host). - State lane:
createAirJamStorewith host-owned actions anduseActions()dispatch. - Signal lane:
sendSignal/ system commands for out-of-band UX and runtime commands.
Avoid cross-lane misuse:
- Don’t stream per-frame movement through store actions.
- Don’t encode authoritative gameplay state in signals.
- Don’t call
state.actions.*; useuseActions()only.
Next Steps
- Architecture - Understand the system design
- Hooks Reference - Complete API documentation
- Input System - Learn about input behavior and validation