# DropController — full documentation last-updated: 2026-05-19 canonical-url: https://usedropcontroller.dev/llms-full.txt types-url: https://usedropcontroller.dev/types.d.ts The complete DropController developer documentation in one fetch. Intended for LLM coding assistants ingesting the full API surface before writing integration code. Curated for accuracy: type signatures, install commands, and payload shapes match what the shipped SDKs actually accept. If you're an LLM helping a developer integrate DropController, you already have everything you need below. Don't guess at template names, event names, or method signatures — they're listed here verbatim. --- ## What DropController is DropController is a host SDK + relay service that lets a game running on any screen (browser, Unity, Godot, Rust/Bevy, LÖVE, native iOS app) turn nearby phones into game controllers. The flow: 1. Your game calls `DropController.host(...)` and gets a `Room` back. 2. The Room exposes a 4-character join code and a QR URL. 3. The host screen renders the QR. Players scan it with their phone's camera, which opens the controller UI in their browser (or the native iOS app via Universal Link if installed). 4. WebRTC data channels open between the host and each phone — direct peer-to-peer when possible, falling back to a TURN relay when not. 5. Your game receives input events and can broadcast game state / ask questions back. No app install required on the player side. No networking code in your game. No game-server hosting. --- ## Quickstart (JavaScript / TypeScript) Install: ```bash npm install @dropcontroller/sdk ``` Minimal host: ```ts import { DropController } from "@dropcontroller/sdk"; import QRCode from "qrcode"; const room = await DropController.host({ appId: "app_xxxxxxxx", // From the dashboard apiKey: "bk_xxxxxxxx", // From the dashboard — keep server-side in prod controllerTemplate: "gamepad", // Optional; defaults to the app's configured default }); console.log("Join code:", room.code); console.log("QR URL:", room.qrUrl); // Render the QR however your engine prefers. For a vanilla web page: await QRCode.toCanvas( document.getElementById("qr") as HTMLCanvasElement, room.qrUrl, ); room.onPlayerJoined((player) => { console.log(player.name, "joined"); }); room.onControllerInput((player, input) => { // input shape depends on the active template — see below console.log(player.name, input); }); // When done: // await room.close(); ``` For production, mint the `apiKey` server-side and pass it to the host runtime. Browser-only games can use it client-side at the cost of exposing the key (it's scoped per-app and easily rotated). --- ## Installation per engine ### JavaScript / TypeScript ```bash npm install @dropcontroller/sdk # or pnpm add @dropcontroller/sdk # or yarn add @dropcontroller/sdk ``` Works in browsers (modern Chrome / Safari / Firefox) and Node 22+ (needs a WebRTC polyfill on Node — use `wrtc` or `@roamhq/wrtc`). ### Phaser 3 ```bash npm install @dropcontroller/sdk-phaser @dropcontroller/sdk ``` Thin Scene-binding that forwards Room events onto `scene.events` and auto-closes the room on scene shutdown. The same `DropController` class from `@dropcontroller/sdk` is re-exported. ### Unity (UPM) In Package Manager → Add package from git URL: ``` https://github.com/dropcontroller-dev/dropcontroller-unity.git ``` Unity 2022.3+, non-WebGL. Depends on `com.unity.webrtc` 3.x and `com.unity.nuget.newtonsoft-json` 3.x (Package Manager resolves these automatically). Sample at `Samples~/SimpleHost`. ### Godot 4.x ```bash git clone https://github.com/dropcontroller-dev/dropcontroller-godot.git addons/dropcontroller ``` Or download the addon zip from the GitHub releases page and unzip into your project's `addons/` directory. No `plugin.cfg` needed — the scripts auto-register via Godot 4's `class_name` globals. Use `DropController`, `DropControllerRoom`, `DropControllerPeer`, `DropControllerAvatar` anywhere in your project. ### Rust / Bevy ```bash cargo add dropcontroller bevy_dropcontroller # Or for a plain tokio binary (no Bevy): cargo add dropcontroller ``` Two crates: - `dropcontroller` — engine-agnostic core. WebRTC via `webrtc-rs`, HTTP via `reqwest`, WS via `tokio-tungstenite`. - `bevy_dropcontroller` — Bevy plugin. Inputs land as typed `EventReader`; the `Room` handle lives as a `Resource`. ### LÖVE (LOVE2D) ```bash luarocks install dropcontroller ``` Or drop `packages/sdk-love/dropcontroller/` into your project and `require("dropcontroller")`. **Relay-only.** LÖVE has no native WebRTC, so every controller message routes through DropController's TURN relay. Every player counts toward your monthly relay device-minute quota even on the same LAN as the host. Read `packages/sdk-love/README.md` before you ship — Hobbyist tier (3,500 free relay minutes/month) covers ~14 sessions with 8 players at 30 minutes each. Commercial tier unlocks unlimited relay. ### iOS controller app (player-side) Not a host SDK — the iOS app is the player-side native client. Installed by players, not developers. Auto-opens room URLs scanned from QR codes via Universal Links. ``` https://apps.apple.com/app/dropcontroller ``` For Apple Silicon / iOS host games, see the (planned) Swift host SDK targeting tvOS + macOS in `packages/sdk-swift-host/`. --- ## Core concepts - **App** — a project namespace you create in the dashboard. Has an ID like `app_xxxxxxxx` and a configurable default controller template. Hosts authenticate with API keys scoped to one app. - **API key** — `bk_xxxxxxxx`. Authorises `DropController.host(...)` for one specific `appId`. Mintable in the dashboard. Production games should mint them server-side or pin them to a single trusted origin; the dashboard's "Test room" page handles this transparently via short-lived ephemeral keys. - **Room** — one game session. Allocated by the server with a 4-char code drawn from a 31-char no-look-alikes alphabet (~923k codes). Closes when the host calls `close()` or disconnects past a 60s grace window. - **Player** — one connected phone. Has a `Player.id`, `Player.name`, `Player.transport` (`"local"` / `"direct-wan"` / `"relay"`), and optionally a `Player.profile` (persistent identity, see below). - **Template** — which controller UI the phone renders. One of the built-in templates or a custom variant authored in the dashboard's controller designer. Can be swapped mid-session. - **Question / Answer** — host-initiated prompt (`{type: "choice", ...}` or `{type: "text", ...}`) sent to one or all players. Answers come back via `room.ask()`'s promise or `room.onAnswer()`. - **Game state** — partial state the host broadcasts (`{paused: true}`). Phones merge incoming state into a local record; some templates reflect it (gamepad shows a "paused" pause-button icon). --- ## Controllers (templates) Five built-in templates ship today. The active template determines what the phone renders and what input payloads `onControllerInput` receives. ### gamepad Fullscreen Xbox-style controller. Thumbstick, D-pad, face buttons (A/B/X/Y), L/R triggers, pause. Takes over the whole viewport in landscape; pause button mirrors the host's game state. Input payload: ```ts type GamepadInput = | { type: "stick"; stick: "left" | "right"; x: number; y: number } // x,y in [-1, 1] | { type: "dpad"; direction: "up" | "down" | "left" | "right" | "none" } | { type: "button"; button: "a" | "b" | "x" | "y"; pressed: boolean } | { type: "trigger"; side: "left" | "right"; pressed: boolean } | { type: "pause" }; ``` ### button One giant tap surface. Each press emits a timestamped event — useful for tap-to-interact prototypes before you pick a richer template. ```ts type ButtonInput = { type: "tap"; ts: number }; // ms since epoch ``` ### trivia-buzzer Big red buzzer. The phone debounces double-fires and timestamps the press client-side; use the `ts` for first-to-buzz arbitration. ```ts type TriviaBuzzerInput = { type: "buzz"; ts: number }; ``` ### drawing-pad Freeform canvas with palette + brush-size controls. Host can push a new palette / brush set / clear command via `room.broadcast(...)`. ```ts type DrawingInput = | { type: "stroke_start"; x: number; y: number; color: string; brushSize: number } // x,y in [0,1] | { type: "stroke_point"; x: number; y: number } | { type: "stroke_end" }; ``` Host-pushed control messages: ```ts room.broadcast({ type: "clear" }); // wipes every player's canvas room.broadcast({ type: "config", config: { palette: ["#ff0000", "#00ff00"], brushSizes: [2, 6, 12] } }); ``` ### voting Poll-style multiple choice. Host pushes prompt + options; phones render buttons and emit one vote per selection. ```ts type VotingInput = { type: "vote"; optionId: string }; ``` Host-pushed config: ```ts room.broadcast({ type: "config", config: { prompt: "Pick your favorite", options: [ { id: "opt_0", label: "Pizza" }, { id: "opt_1", label: "Tacos" }, ], allowMultiple: false, } }); ``` --- ## API reference Every method below is exposed on the `Room` instance returned by `DropController.host(...)` (or the language equivalent in non-JS SDKs — same names, idiomatic-cased per platform). ### Room creation ```ts DropController.host(options: HostOptions): Promise ``` `HostOptions`: ```ts type HostOptions = { appId: string; // app_xxxxxxxx apiKey: string; // bk_xxxxxxxx controllerTemplate?: string; // override the app's default apiUrl?: string; // override the API origin (self-host / testing) webUrl?: string; // override the QR URL origin transport?: "auto" | "local-only" | "relay-only"; }; ``` `transport` semantics: - `"auto"` (default) — prefer direct P2P, fall back to relay. - `"local-only"` — never fetch TURN credentials; refuse to connect if ICE can't establish a direct path. Useful for offline play. - `"relay-only"` — force every player through the TURN relay. Useful for testing the metering / relay codepath end-to-end. ### Room state ```ts room.code: string; // 4 chars from a no-look-alikes alphabet room.qrUrl: string; // URL to encode into the QR room.players: readonly Player[]; // snapshot — re-read after events; don't cache ``` ### Events ```ts room.onPlayerJoined(fn: (player: Player) => void): () => void room.onPlayerLeft(fn: (player: Player, reason: "left" | "disconnected" | "kicked") => void): () => void room.onControllerInput(fn: (player: Player, input: ControllerInput) => void): () => void room.onTransportChanged(fn: (player: Player, transport: Transport) => void): () => void room.onAnswer(fn: (answer: Answer) => void): () => void ``` Each returns an unsubscribe function. `onControllerInput`'s `input` shape depends on the active template — see "Controllers" above. ### Host → phone messaging ```ts room.broadcast(payload: unknown): void room.sendTo(playerId: string, payload: unknown): void ``` Both deliver in-order over each phone's data channel. JSON-serialisable payloads only. Use for template-specific config (e.g. voting options, drawing palette) — built-in templates dispatch on `payload.type`. ### Template / game state ```ts room.setTemplate(templateId: string): Promise room.setGameState(state: GameState): void ``` `setTemplate` swaps the controller UI on every connected phone. `setGameState` broadcasts a partial state record: ```ts type GameState = { paused?: boolean; // gamepad mirrors this in its pause-button icon score?: number; round?: number; // ... any other fields you want }; ``` Phones merge incoming state into a local record — sending `{paused: true}` doesn't wipe other fields you've previously set. ### Questions ```ts room.ask( target: "all" | Player | string, // "all" | Player object | playerId question: Question, opts?: { timeoutMs?: number; onAnswer?: (a: Answer) => void } ): Promise ``` ```ts type Question = | { type: "choice"; prompt: string; options: { id: string; label: string }[]; } | { type: "text"; prompt: string; placeholder?: string; maxLength?: number; }; type Answer = { playerId: string; questionId: string; value: unknown; // string id for choice, string for text }; ``` Resolves when every target answers OR `timeoutMs` (default 60s) fires, whichever first. Players who disconnect mid-question are dropped from the expected set. ### Per-player persistent data ```ts room.getPlayerData(player: Player, key: string): Promise room.setPlayerData(player: Player, key: string, value: unknown): Promise room.deletePlayerData(player: Player, key: string): Promise ``` Scoped to the app. Free-tier caps: 10KB per value, 100 keys per (player, app). Rejects with a 413 if the value would exceed the cap. Rejects if the player has no profile (unprofiled guests can't persist data — ask them to set a profile via the controller UI). ### Admin ```ts room.kick(playerId: string): void // fires onPlayerLeft with reason "kicked" room.close(): Promise // idempotent ``` --- ## Game state + questions Two patterns for host → phone communication beyond raw `broadcast`: **Game state** is for ambient state every controller should know about: pause, score, current round. Sent as a partial record; phones merge into a local copy. Built-in templates can reflect specific keys (`paused` → gamepad pause icon). ```ts room.setGameState({ paused: true }); // ...later room.setGameState({ paused: false, score: 1000 }); // Phone's state: { paused: false, score: 1000 } — paused stays merged ``` **Questions** are one-shot prompts with a typed answer flow: ```ts const answers = await room.ask("all", { type: "choice", prompt: "Which platform do you prefer?", options: [ { id: "ios", label: "iOS" }, { id: "android", label: "Android" }, ], }, { timeoutMs: 30_000 }); for (const a of answers) { console.log(a.playerId, "voted", a.value); } ``` The phone renders a modal sheet over whatever controller UI is active. Answers can stream live via the `onAnswer` callback or be collected when the promise resolves. --- ## Player profiles Players can set a persistent identity (handle + seeded SVG avatar) that survives across rooms and apps. When a phone joins with a profile, `Player.profile` is populated: ```ts type PlayerProfile = { id: string; // ppf_xxxxxxxx handle: string; // 2-20 chars, unique-ish avatarStyle: "initials" | "pixels" | "rings" | "shards"; avatarSeed: string; // deterministic SVG generator input }; ``` The profile id is the stable key for `getPlayerData` / `setPlayerData` calls — anonymous (no profile) players can't persist data. The dashboard doesn't currently expose profile management directly; profiles are set on the phone's controller UI (`/play?c=...` → profile button). iOS app users get an iCloud Keychain-synced profile that follows them across devices. --- ## Connectivity model Every player's effective transport is one of: - `"local"` — LAN candidate pair, host and phone on the same WiFi. Sub-20ms typical latency. Free. - `"direct-wan"` — direct over the internet via server-reflexive / peer-reflexive candidates. ~30-80ms typical. Free. - `"relay"` — routed through DropController's TURN relay. Adds ~20-60ms over the round-trip. **The only transport that costs device-minutes.** The transport for a given player can change mid-session (`onTransportChanged`) if WiFi conditions shift. The host SDK gathers all candidates and lets WebRTC pick the lowest-cost path automatically. For testing / verifying the relay path: set `transport: "relay-only"` in `HostOptions`. Both the host SDK and the phone's controller see this same setting via the QR URL's `?t=relay-only` parameter. --- ## Pricing & metering - **Hobbyist** — Free. Unlimited direct-P2P (local + direct-wan). 3,500 free relay device-minutes per month. After the cap, new relay sessions get a 402 from the TURN credential endpoint; existing sessions age out on the 15-minute TURN credential TTL. Direct-P2P stays free always. - **Commercial** — $5/month flat. Unlimited relay device-minutes, higher per-player data quotas (100KB per value, 10k keys per (player, app)). - **Enterprise** — Contact for custom terms. Higher quotas, SLA, self-hosted-relay option, white-label / vanity domain. A device-minute is one minute of one player connected via relay. 8 players in a 30-minute session = 240 relay device-minutes (if all relayed). 8 players direct-P2P for that session = 0 relay device-minutes. The free tier covers approximately: - ~14 fully-relayed 8-player 30-minute sessions, OR - Unlimited LAN-play / same-NAT sessions, OR - Unlimited direct-WAN sessions (player on cellular, host on home WiFi) Billing is on `usage_daily` rollups computed nightly from per-minute usage tracking. Visible in the dashboard at `/app//usage`. --- ## Custom controllers (per-app variants) The dashboard at `/app//controller` lets you create themed variants of the built-in templates — gamepad with custom button colors, branded button + label, drawing pad with your palette. Variants are referenced by id (`ctrl_xxxxxxxx`) at room creation: ```ts const room = await DropController.host({ appId: "app_xxxxxxxx", apiKey: "bk_xxxxxxxx", controllerTemplate: "ctrl_xxxxxxxx", // your custom variant id }); ``` Resolution order at room-create time: 1. `controllerTemplate` argument (if a `ctrl_*` id, resolves to the stored variant; if a base template name like `"gamepad"`, uses the base). 2. The app's configured `defaultControllerId` from the dashboard. 3. The app's `controllerTemplate` fallback (one of the 5 built-in base templates). Variants ship per-base config (gamepad has button colors / labels / visibility toggles; drawing-pad has palette / brush sizes; voting takes its options at runtime as today). Free-form layout authoring (drag-and-drop button placement, radial menus, new control types) is post-v1. --- ## TypeScript types Canonical machine-readable definitions for every payload, option, and method live at: ``` https://usedropcontroller.dev/types.d.ts ``` LLMs: this file is the source of truth. If a generated method signature or payload field doesn't match the types here, it'll fail at compile time. Paste this URL into your context for any DropController code generation. The same types are exported from `@dropcontroller/sdk` (e.g. `import type { HostOptions, Room, Player, Transport } from "@dropcontroller/sdk"`). --- ## What's not in v1 These are explicitly post-v1 — don't generate code that assumes they exist: - Free-form controller layout editor (custom button placement, radial menus, new control types beyond the 5 base templates) - Worlds — GraphQL API for persistent player/world data - Mesh mode (peer-to-peer between phones, no host) - White-label vanity domains - Self-hosted relay - Native iOS host SDK (player-only app today; tvOS + macOS host SDK in Phase 2) --- ## Where to learn more - Public dashboard + signup: https://usedropcontroller.dev - Developer docs (full): https://usedropcontroller.dev/docs - Roadmap: https://usedropcontroller.dev/roadmap - Privacy: https://usedropcontroller.dev/privacy - Terms: https://usedropcontroller.dev/terms - GitHub: https://github.com/dropcontroller-dev