Turbo Native, for the desktop
Your Rails app is already the source of truth. Turbo Desktop wraps it in a thin native shell built on Tauri 2 — the same path configuration, bridge components, and progressive enhancement you know from Hotwire Native, now on macOS, Windows, and Linux.
Why it exists
Rails developers have had turbo-ios and turbo-android for years. Desktop was the gap. Turbo Desktop fills it without asking you to learn a new UI framework: your existing views, frames, and controllers keep working, and you reach for native only where it pays off.
Quick start
Four steps from an existing Rails app to a running desktop shell. You need Rails 7+, Ruby 3.1+, and a Rust toolchain.
Scaffold the shell
npx turbo-desktop new myapp cd myapp && npm install cargo install tauri-cli
Point it at your Rails server
{
"server_url": "http://localhost:3000",
"app_name": "My App",
"path_configuration_url": "http://localhost:3000/turbo-desktop/path-configuration.json"
}
Leave path_configuration_url out and it defaults to {server_url}/turbo-desktop/path-configuration.json.
Teach Rails about the shell
# Gemfile
gem "turbo_desktop-rails"
# bundle install
# rails generate turbo_desktop:install
# config/routes.rb
get "/turbo-desktop/path-configuration",
to: "turbo_desktop#path_configuration"
<%# app/views/layouts/application.html.erb %> <% turbo_desktop_only do %> <div class="titlebar-drag-region"></div> <% end %> <% turbo_web_only do %> <nav class="web-navbar">...</nav> <% end %>
// app/javascript/application.js
if (window.TurboDesktop?.isNative) {
document.documentElement.dataset.shell = "desktop"
}
Run both processes
bin/rails server # your app cargo tauri dev # the shell
The window opens on your server_url. Every Turbo visit from here on is proposed to the shell before it happens.
Path configuration
A JSON document, served by Rails, that maps URL patterns to presentation rules. Same concept and the same last-match-wins semantics as turbo-ios and turbo-android — the shell stays dumb, your server decides how screens appear.
{
"settings": { "screenshots_enabled": true },
"rules": [
{ "patterns": ["/"], "properties": { "presentation": "default" } },
{ "patterns": ["/new$", "/edit$"], "properties": { "presentation": "modal", "title": "Edit" } },
{ "patterns": ["/reports/"], "properties": { "presentation": "new_window" } },
{ "patterns": ["/settings"], "properties": { "presentation": "native" } }
]
}
See each presentation
Pick a rule to watch what the shell does with the window.
Bridge components
The Bridge is the desktop equivalent of Strada: structured messages between a Stimulus controller in the WebView and a handler in Rust. Write the controller you would write for mobile, name the component, and the shell routes it.
A notification, end to end
// app/javascript/controllers/notify_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends TurboDesktop.stimulusBridge(Controller, "notification") {
connect() {
super.connect()
this.sendBridge("connect", { title: "My App" })
}
notify(event) {
this.sendBridge("connect", {
title: "New Message",
body: event.target.dataset.body
})
}
receiveBridge(message) {
console.log("Native says:", message)
}
}
<%# Attach bridge data attributes to any element %>
<%= tag.button "Export PDF",
**turbo_desktop_bridge("menu-item",
title: "Export PDF",
shortcut: "Cmd+E") %>
// npm install @turbo-desktop/bridge
import { stimulusBridge, isTurboDesktop } from "@turbo-desktop/bridge"
if (isTurboDesktop()) {
const info = await TurboDesktop.getWindowInfo()
console.log(`Running on ${info.platform}`)
}
The turbo-desktop.js IIFE is injected into every page by the shell. The npm package only adds types and ESM exports over the same globals — nothing is bundled twice.
Dev Inspector
In development, press Cmd/Ctrl Shift D to open an in-app overlay over your running app.
# config/initializers/turbo_desktop.rb config.inspector_enabled = Rails.env.development? # app/views/layouts/application.html.erb, in <head> # <%= turbo_desktop_inspector_meta_tag %>
Need it against a build you cannot rebuild? Flip it on at runtime with localStorage.setItem("td:inspector", "1").
Rails gem
The turbo_desktop-rails gem gives your app shell awareness. Detection reads the User-Agent, e.g. Turbo Desktop/0.0.1 (macOS; aarch64).
# config/initializers/turbo_desktop.rb
TurboDesktop.configure do |config|
config.path_configuration = {
settings: { screenshots_enabled: true },
rules: [
{ patterns: ["/"], properties: { presentation: "default" } },
{ patterns: ["/new$", "/edit$"], properties: { presentation: "modal" } },
{ patterns: ["/settings"], properties: { presentation: "native" } }
]
}
end
JavaScript API
Everything hangs off window.TurboDesktop, injected by the shell on every page load. Outside the shell it is undefined — guard with isTurboDesktop().
CLI
Scaffolding for a new shell, no clone required.
npx turbo-desktop new myapp # scaffold a shell npx turbo-desktop dev # run the shell against your server npx turbo-desktop build # produce a signed platform bundle
How it compares
Same mental model as the mobile shells, different runtime underneath.