iRaceDeck is an Elgato Stream Deck, Mirabox, and Ulanzi Deck plugin for iRacing. Turn your Stream Deck, Mirabox, or Ulanzi Deck into a fully-featured button box with live telemetry, pit controls, camera management, and more.
- Features
- Installation
- Project Structure
- Releasing
- Contributing
- Troubleshooting
- Usage and license
- For companies
- Inspiration
- Acknowledgements
33 actions with 265+ modes across 9 categories:
| Category | Actions | Modes | Examples |
|---|---|---|---|
| Display & Session | 2 | 10 | Incidents, laps, position, iRating, gaps ahead/behind, fuel, flags |
| Driving Controls | 6 | 32 | AI spotter, audio (incl. Race Engineer & Radar volume), black box cycling, look direction, car control, pit crew |
| Cockpit & Interface | 5 | 35 | Wipers, FFB, splits & reference, telemetry, UI toggles |
| View & Camera | 6 | 95 | FOV, replay, replay markers, camera controls, broadcast tools |
| Media | 1 | 7 | Video recording, screenshots |
| Pit Service | 3 | 15 | Fuel (button and dial), tires, compounds, tearoff, fast repair |
| Car Setup | 7 | 44 | Brakes (button and dial), chassis, aero, engine, fuel mix, hybrid/ERS, traction control |
| Communication | 2 | 34 | Chat, macros, whisper, reply, race admin commands |
| Stream Deck | 1 | 1 | Switch to bundled iRaceDeck profiles (Elgato only) |
Key highlights:
- Live telemetry at 4 Hz with automatic iRacing connection/reconnection
- Pit Crew action with Radar directional proximity ticks and a spoken Race Engineer, both through a multi-channel audio mixer (miniaudio); no voice ships inside the plugin — every one is a downloadable pack, and the plugin installs and updates its own at launch
- All keyboard shortcuts are user-configurable via the Property Inspector
- SDK-first design: uses iRacing broadcast commands where possible, keyboard simulation only as fallback
- Native C++ addon for low-latency Win32 API access (iRacing SDK, keyboard input, audio engine)
- Download the latest
.streamDeckPluginrelease - Double-click to install
- Find iRaceDeck in the Stream Deck action list
Prerequisites:
- Windows 10+ (iRacing is Windows-only)
- Node.js 24+
- pnpm 10+ — it switches itself to the version pinned in
package.json - Python 3.x and Visual Studio Build Tools with the C++ workload (for the native addon)
- Elgato Stream Deck software
git clone https://github.com/niklam/iracedeck.git
cd iracedeck
pnpm install
pnpm build# Build only the Stream Deck plugin packages
pnpm build:stream-deck
# Watch mode with hot-reload (restarts Stream Deck automatically)
pnpm watch:stream-deck
# Run tests: all of them, a path filter, or one package
pnpm test
pnpm test packages/iracing-sdk/
pnpm --filter @iracedeck/deck-core test
# Lint and format
pnpm lint:fix
pnpm format:fix
# Hear a Race Engineer voice edit in the sim: opt this machine in once
# (then open a new terminal), and every `pnpm build` stages the voice packs
# and points the plugins at them. Build with the deck host stopped; while it
# runs, restage after a voice edit and press Rescan voices (no restart)
setx IRACEDECK_DEV_VOICES 1
pnpm stage:voices
# Override it for one worktree: `off` plays the real download, `auto` follows
# the variable again, `on` turns it on without the variable
pnpm dev:voices offA pnpm monorepo built with Turborepo:
packages/
iracing-actions/ Platform-agnostic iRacing action implementations
deck-adapter-elgato/ Elgato Stream Deck adapter (bridges Elgato SDK to deck-core)
deck-adapter-mirabox/ Mirabox VSD Craft adapter (WebSocket protocol to deck-core)
deck-adapter-ulanzi/ Ulanzi Deck adapter (UlanziStudio WebSocket protocol to deck-core)
deck-core/ Platform-agnostic base classes, types, and shared utilities
icon-composer/ Standalone SVG icon assembly (zero dependencies)
icons/ SVG icon templates (Mustache)
iracing-native/ C++ N-API addon (shared memory, window messaging, scan codes)
iracing-sdk/ TypeScript SDK (telemetry, broadcast commands, session parsing)
logger/ Shared logger interface
iracing-plugin-mirabox/ Mirabox device plugin
iracing-plugin-ulanzi/ Ulanzi Deck device plugin
pi-components/ Shared Property Inspector assets (web components, EJS templates, partials, data)
iracing-plugin-stream-deck/ Elgato Stream Deck plugin
website/ Documentation website (iracedeck.com)
| Package | Role |
|---|---|
@iracedeck/iracing-actions |
All 32 action implementations, platform-agnostic |
@iracedeck/deck-core |
Base classes, types, keyboard service, icon templates, global settings, settings window |
@iracedeck/deck-adapter-elgato |
Bridges the Elgato SDK to deck-core's IDeckPlatformAdapter interface |
@iracedeck/deck-adapter-mirabox |
Bridges the Mirabox VSD Craft WebSocket protocol to deck-core |
@iracedeck/deck-adapter-ulanzi |
Bridges the UlanziStudio WebSocket protocol to deck-core |
@iracedeck/icon-composer |
Standalone SVG icon assembly (pure functions, zero dependencies) |
@iracedeck/icons |
SVG icon Mustache templates with colorization support |
@iracedeck/iracing-native |
C++ Node.js addon for Win32 APIs (memory-mapped files, window messaging, scan-code input) |
@iracedeck/iracing-sdk |
TypeScript SDK for reading telemetry and sending iRacing broadcast commands |
@iracedeck/logger |
Shared logging interface with scoped loggers |
@iracedeck/pi-components |
Shared PI web components, EJS partials, Rollup EJS plugin, and the Ulanzi + settings-window bridges |
@iracedeck/iracing-plugin-stream-deck |
Elgato Stream Deck plugin — registers actions, PI templates, manifest |
@iracedeck/iracing-plugin-mirabox |
Mirabox plugin — registers the same actions for Mirabox devices |
@iracedeck/iracing-plugin-ulanzi |
Ulanzi Deck plugin — registers the same actions for Ulanzi devices |
@iracedeck/website |
Documentation website at iracedeck.com |
Button press (Stream Deck, Mirabox, or Ulanzi Deck)
-> adapter (deck-adapter-elgato / deck-adapter-mirabox / deck-adapter-ulanzi)
-> actions (platform-agnostic action handler)
-> deck-core (keyboard service / SDK commands)
-> iracing-sdk (broadcast command) or iracing-native (scan-code keystroke)
-> iRacing
iRacing telemetry (shared memory)
-> iracing-native (reads memory-mapped file)
-> iracing-sdk (parses telemetry buffer, 4 Hz update loop)
-> deck-core (notifies subscribers)
-> actions (updates button display via adapter)
All packages share a single version number, driven by release-it plus one custom hook. Nothing is inferred from commit messages — you choose the version, and release-it asks before it commits, tags, pushes, or creates a GitHub Release.
pnpm release # prompts for the version
pnpm release -- 3.2.0 # release a specific version
pnpm release:dry # safe preview — writes nothingpnpm release wraps release-it (scripts/release.mjs): it strips the -- separator pnpm inserts, exports GITHUB_TOKEN from gh auth token when that variable is not already set, and signals a dry run to the hook below.
A release then runs in this order:
- The
before:bumphook (scripts/release-hooks.mjs) bumps everypackages/*/package.json. The rootpackage.jsonis bumped separately, by release-it's own npm plugin. - The same hook bumps all three plugin
manifest.jsonfiles (Stream Deck, Mirabox, Ulanzi). It discovers them rather than using a hardcoded list, and writes a 4-partmajor.minor.patch.buildversion — Elgato's manifest schema rejects semver suffixes — with the build slot taken fromgit rev-list --count HEAD. - On a stable version only, the hook stamps the
_Unreleased_line of the matching## <version>section inpackages/website/src/content/docs/changelog.mdxwith today's date, and regeneratespackages/iracing-actions/src/actions/data/changelog.jsonfrom the stamped text. Any version containing a dash (3.2.0-dev.0,3.2.0-rc.1) skips both, as does a section that is missing or already dated. - release-it commits the result as
chore(release): vX.Y.Z. - It creates the annotated tag
vX.Y.Z. - It pushes the branch and the tag to origin.
- It creates a GitHub Release named
vX.Y.Z.
Steps 4–7 are each prompted outside CI, and declining one skips it. Development bumps are normally committed without a tag or a GitHub Release, which is why most -dev.0 commits in the history are untagged. Pushing a v* tag is what triggers the packaging workflow below, so a bump you chose not to tag publishes nothing.
The changelog is not generated. It is written by hand in changelog.mdx as part of each feature's own PR (see .claude/rules/changelog.md), and the release only stamps its date. GitHub Release notes are generated by GitHub from merged PR labels (github.autoGenerate, categorised by .github/release.yml), not from the changelog.
Preview with
pnpm release:dry, nevernpx release-it --dry-run. release-it runsbefore:bumphooks even during a dry run, and only the pnpm wrapper sets the environment flag the hook checks for. Invoking release-it directly with--dry-runreally does rewrite and stage every package and manifest file, plus the changelog whenever step 3's stamp applies.
.github/workflows/release-pack.yml is a reusable workflow invoked by release.yml (which runs automatically when a version tag is pushed). It:
- Builds the full monorepo once on Windows
- Packs every published Race Engineer voice pack and checks it against its committed catalog entry (see below)
- Packs all three plugins (Stream Deck, Mirabox, Ulanzi) from that single build using
streamdeck pack - Attaches each packed plugin to the GitHub Release
Race Engineer voices are distributed as downloadable packs (packages/audio-assets/src/build/voice-packs.mjs is the list). Their archives are never uploaded by hand: scripts/publish-voice-packs.mjs runs inside the tag workflow above, packs every published voice through the same pipeline the plugin build uses, verifies the archive's sha-256 and size against the committed packages/audio-assets/catalog/<id>.json, and attaches it to the voices-<id>-<version> GitHub release, created with --latest=false so it never takes the "latest" slot the plugin download links resolve through. A pack whose version has not changed is found already published and skipped; an archive the runner cannot reproduce, or a pack whose bytes changed without a version bump, fails the job before anything is published. Every website deploy — the release path and any manual one — first asks each catalog entry's URL and refuses to build if an archive answers anything but 200, so voice-catalog.json can never go live naming an archive that does not exist.
A pack carries more than clips: each voice's callout script (voice/<id>/callouts.json, extracted from configs/<id>.voice.json by pnpm generate:callout-scripts) travels in the archive beside them. When a pack's clips or script change: bump its version, run pnpm --filter @iracedeck/audio-assets pack:voice, and commit the regenerated catalog entry. The next release publishes it.
Writing a pack — ours or anyone's — is documented on the site under Voice Packs: the concept, a first-pack tutorial, the callouts.json format, and a reference of every callout, every name a script may use and the full recording script, generated from the catalog by pnpm generate:pack-reference; from a clone of the repo, pnpm lint:pack <packDir> (after pnpm build) reports everything in a pack folder that the plugin would otherwise skip quietly.
.github/workflows/publish-voice-packs.yml is the manual path, dispatched from the Actions tab. With publish off (the default) it is a dry run — pack and verify, upload nothing, attach the archives as a voice-packs artifact — worth running once before tagging a release that changes a pack. With publish on it publishes the archives and then deploys the website so the catalog offers the new version, for a pack that has to ship between plugin releases. That deploy builds the site from the latest stable plugin tag with only packages/audio-assets/catalog/ taken from the dispatched branch, so documentation for unreleased work on master never reaches the site early.
- A clean working tree — release-it refuses to start otherwise (
requireCleanWorkingDir) - GitHub CLI (
gh) authenticated —pnpm releasereads a token fromgh auth tokenunlessGITHUB_TOKENis already set. Only the GitHub Release step needs it, so declining that prompt works without either
Contributions are welcome! Here's how to get started:
- Fork the repo and create a branch (
feature/123-your-feature) - Follow conventional commits with package scope (e.g.
feat(iracing-plugin-stream-deck): add new action) - Add tests for new code (Vitest)
- Make sure
pnpm buildandpnpm testpass - Open a pull request
Actions live in packages/iracing-actions/src/actions/<action-name>/, one folder per action. Each action needs:
<action-name>.ts— action class extendingConnectionStateAwareActionfrom@iracedeck/deck-core<action-name>.test.ts— unit tests<action-name>.ejs— Property Inspector template (compiled toui/<action-name>.html)icon.svg+key.svg— static category and key icons (copied into each plugin'simgs/actions/<name>/at build time)- Mustache SVGs in
packages/icons/<action-name>/for any dynamic variants - Registration in both
packages/iracing-plugin-stream-deck/src/plugin.tsandpackages/iracing-plugin-mirabox/src/plugin.ts - Manifest entry in each plugin's
manifest.json - Entries in
packages/iracing-actions/src/actions/data/{key-bindings,docs-urls,icon-defaults}.jsonwhere applicable
See the existing actions for reference, or check packages/iracing-plugin-stream-deck/CLAUDE.md for step-by-step instructions.
| Problem | Solution |
|---|---|
Double-clicking .streamDeckPlugin doesn't install |
Rename the file to add .zip at the end, extract the contents to %APPDATA%\Elgato\StreamDeck\Plugins, and restart Stream Deck |
| Plugin doesn't connect | Make sure iRacing is running and you're in a session (on track) |
| Buttons show nothing | iRacing telemetry is only available while driving; the plugin reconnects automatically |
| Native addon build fails | Install Python 3.x and VS Build Tools with C++ workload. Try npm config set msvs_version 2022 |
| Key presses don't work | Check your key bindings in the Property Inspector match your iRacing configuration |
iRaceDeck is source-available and licensed under the iRaceDeck Non-Commercial License v1.1.
Free for personal and non-commercial use, including use in sim racing events and competitions.
Plugin downloads ship the license alongside THIRD-PARTY-LICENSES.md, which aggregates the license notices of every bundled third-party component.
For full details, see USAGE.md.
The name “iRaceDeck” and associated branding are not included in the license and may not be used for derived versions without permission.
If you're unsure whether your use case is allowed, feel free to reach out.
iRaceDeck is source-available rather than open source.
The goal is simple:
- Keep iRaceDeck free for sim racers, hobbyists, and the community
- Allow people to build on top of it (profiles, icons, tools, integrations)
- Prevent others from selling or commercializing the plugin itself without permission
- Require that anyone who distributes a modified version of iRaceDeck makes their modifications publicly available, so the community keeps benefiting from improvements
If you're just using, modifying, or contributing to iRaceDeck, nothing changes for you.
If you're building something commercial around iRaceDeck, that's usually fine — just don't redistribute or sell the plugin itself. If you're unsure, feel free to reach out.
If you're working on something commercial based on iRaceDeck, feel free to reach out.
I'm open to licensing, collaboration, or helping you build on top of it.
Contact: niklas@iracedeck.com
This project was inspired by iRaceIT, a Stream Deck plugin for iRacing.
- Elgato Stream Deck SDK
- iRacing SDK
- Node-API (N-API)
- pyirsdk (reference implementation)
