RubyEverywhere
Ship desktop and mobile apps from the Ruby web apps you already know how to build.
rubyeverywhere.com · by Andrea Fomera
RubyEverywhere takes a Rails (or Sinatra, or Hanami) app and gives it a native life on the platforms your users are already on:
- Desktop — a Tauri-based shell. In local mode your whole app is pressed into a single-file binary with Tebako and runs on-device, no Ruby install required; in remote mode the shell is a thin native wrapper around your deployed app.
- iOS — a Hotwire Native shell with a real tab bar, nav-bar buttons, menus, action sheets, notifications, haptics, biometrics, deep links, and native sign-in.
- Android — a feature-for-feature Hotwire Native Android port of the same
shell, stamped from the same
config/everywhere.yml.
One config file, one CLI, one JavaScript API. The web app stays the source of truth — most of what you'd normally rebuild natively (tabs, routes, menus, badges) deploys with your Rails app rather than through an app-store review.
Install
gem install ruby_everywhere
This gives you two equivalent executables — every and rbe — plus the
packaging toolchain (the rbe-tebako fork) as a dependency.
Or add it to your app's Gemfile, which also wires up the Rails engine (bridge asset serving, importmap pin, view helpers, the live mobile config endpoint):
gem "ruby_everywhere"
Requires Ruby >= 3.2.
The gem is free, and it always will be. Everything here — the CLI, the desktop shell, both mobile shells, the bridge — is MIT licensed and yours to use, forever, with no account and no key. Local builds run on a Mac today (pressing, code signing, Xcode and the Android SDK all want a macOS host). If you don't own one, or you'd rather not keep a build toolchain healthy, the RubyEverywhere Platform is a paid service coming soon that runs the exact same builds for you — see Hosted builds below. It's a convenience, never a gate: nothing in this gem is held back for it.
Quick start
cd path/to/your/app
every install # remote mode: a native shell around your deployed app
every install --local # local mode: compile and ship the app itself
every install --agents # ...plus an AGENTS.md guide for AI coding agents
every doctor # check the toolchain before your first build
every dev # dev server + native shells on demand, live reload
every dev --ios # ...straight into the iOS Simulator
every dev --android # ...straight into the Android emulator
every dev --mobile # ...both at once, built in parallel
every build # package the macOS .app
every build --ios # build the iOS shell
every build --android # build the APK/AAB
every install is idempotent — it adds config/everywhere.yml, a boot stub,
and the framework-specific config tweaks it needs, and skips anything already
in place. For Rails it does the surgery for you; for Sinatra and Hanami it
vendors the bridge and prints the remaining wiring rather than guessing at your
boot files.
Working with an AI coding agent? --agents writes an AGENTS.md — the format
Claude Code, Cursor, Copilot and Codex all read — covering the view helpers
(native_app?, everywhere_nav_button, everywhere_fab, the menus and badges),
config/everywhere.yml, the bridge API, and the every commands, so an agent
reaches for the right helper instead of hand-rolling markup. It's framework- and
mode-aware, and adds a one-line CLAUDE.md pointer if you don't already have
one. If you already have an AGENTS.md, only the block between
<!-- rubyeverywhere:begin --> and <!-- rubyeverywhere:end --> is written, so
re-running refreshes our section and leaves your own notes alone.
every dev never packages anything: it runs your ordinary dev server and points
the native shells at it, so the edit-refresh loop is untouched Rails.
It keeps a dock pinned to the bottom of the terminal showing what each target is doing, while the logs scroll normally above it — ordinary scrollback, so you can still scroll up and search a backtrace:
web │ Started GET "/notes" for 127.0.0.1
ios │ ▸ compiling Swift sources
────────────────────────────────────────────────────────────────
● web :3000 ○ desktop ⠙ iOS 42s ✗ Android see dist/android-build.log
⌨ d desktop · i iOS · a Android · b browser · l logs · r restart · ? help · q quit
Every target runs on its own thread, so builds happen in parallel and the keys
keep working while one is going. A build that fails marks that target red and
leaves everything else — the dev server included — running; fix the error and
press the key again. r restarts just the dev server, ? expands the full key
list, and each line is tagged with the source that printed it once more than one
thing is talking.
Set EVERY_NO_DOCK=1 (or pipe the output anywhere) to get plain, unadorned
lines instead.
Configuration
Everything per-app lives in config/everywhere.yml. The native shell templates
are frozen — the CLI stamps a copy per app and only ever writes the identity,
config, icon, and color files, so a shell upgrade never collides with your app.
app:
name: My Really Awesome App
bundle_id: com.example.myapp # set it once; renaming moves users' data
version: 1.2.0
entry_path: /dashboard # where the app opens
icon: icon.png
appearance:
tint_color: "#CC342D"
background_color: # one hex string, or split by system theme
light: "#FAF9F7"
dark: "#1C1B1A"
remote:
url: https://app.example.com # remote mode: the deployed app to wrap
window: # desktop window chrome
title_bar: overlay # decorated (default) | overlay | frameless
title: false # draw the title text
size: [1100, 750]
min_size: [600, 400]
drag_height: 28 # top strip that drags the window; 0 to opt out
tabs: # the native mobile tab bar
- name: Builds
path: /builds
icons:
ios: hammer # SF Symbol
android: build # Material icon
permissions: # only declared permissions can ever prompt
notifications: true
camera: "Scan QR codes to pair devices." # the sentence iOS shows
rules: # Hotwire Native path configuration
- patterns: ["/preferences$"]
properties:
context: modal
native: # code compiled into the native shells
desktop:
commands: [scan_ports] # Rust in native/desktop/, callable from the page
crates:
serialport: "4.3"
build:
ruby: "4.0.6"
targets: [macos-arm64, ios-arm64, android-arm64]
updates: # self-hosted, signed auto-updates
url: https://updates.example.com
public_key: RWQ…
auto: check # off | check | download | install
Also supported: auth: (native OAuth sign-in with a callback scheme),
deep_linking: (universal links / App Links — the gem serves the
apple-app-site-association and assetlinks.json files for you), and
native: for "supernative" extensions — Swift you keep in your app repo under
native/ios/ that every build compiles into the shell, including SwiftUI
screens, bridge components, a custom splash, and pinned SPM packages.
Desktop has the same seam in native/desktop/, shaped for what a desktop app
actually wants: not native screens (the whole UI is a webview) but the machine
underneath it. Drop in Rust, pin the crates you need, and call it from the page:
native:
desktop:
commands: [scan_ports]
setup: true # run native/desktop/setup.rs at boot
crates:
serialport: "4.3"
// native/desktop/serial.rs
pub fn scan_ports(_app: &tauri::AppHandle, payload: serde_json::Value)
-> Result<serde_json::Value, String> { … }
const ports = await Everywhere.desktop.invoke("scan_ports", { baud: 9600 })
Declaring anything under native.desktop has a real cost the mobile sections
don't: the desktop shell stops being one prebuilt binary shared by every app and
gets compiled per app, so the first build takes minutes instead of seconds. Apps
that declare nothing are untouched.
Images the desktop shell itself draws live in native/desktop/assets/, using the
same <name>[@2x|@3x][~dark].png filenames the mobile assets folders use.
Name one tray (or tray-template for a macOS template image) and it becomes
the menu-bar icon; a custom splash: can reference the rest as
assets/logo.png. These are for the shell, not your pages — images your HTML
references should keep coming from the asset pipeline.
window: shapes the desktop frame. title_bar: overlay is the macOS look where
the traffic lights float over your page and there's no title text — pair it with
the .everywhere-titlebar-inset class from everywhere/native.css, which
reserves exactly the space they sit in. frameless removes the system chrome
altogether; the page then draws its own bar and drives the buttons through
Everywhere.window. Windows and Linux have no overlay style, so overlay
behaves as frameless there.
Either way the window stays draggable with no markup: the shell makes the top
28px of the page drag it (and double-click zoom it), while buttons and links up
there keep their clicks. window.drag_height: 0 hands that back to you.
Tabs and path rules ship two ways from that one source: baked into the app
bundle for offline/first launch, and served live from
/everywhere/{ios,android}_v1.json, so changing them deploys with your web app.
Everywhere.filter_tabs lets you vary the tab bar per request — it shares the
app's session, so signed-out users can get a different bar without a release.
The JavaScript bridge
The gem ships @rubyeverywhere/bridge — one API for the browser, the desktop
shell, and the mobile shells. Rails apps get it served and importmap-pinned
automatically; Sinatra and Hanami get it vendored to public/bridge.js.
import Everywhere from "@rubyeverywhere/bridge"
Everywhere.platform // "desktop" | "mobile" | "browser"
Everywhere.native // true inside a RubyEverywhere app
Everywhere.notify({ title, body })
Everywhere.menu({ title, items })
Everywhere.haptics.impact("medium")
Everywhere.badge.setTab("/inbox", 3)
Everywhere.permissions.request("camera")
Everywhere.storage.set("theme", "dark")
Everywhere.updates.check()
Everywhere.window.close() // desktop, for a page-drawn title bar
Everything degrades gracefully — the same page works in a plain browser tab and
gains native powers inside the app. See
bridge/README.md for the full surface.
Most native chrome needs no JavaScript at all. Tag ordinary markup and the
bridge lifts it into real native controls; tapping the native control just
.click()s the element it mirrors, so behavior stays defined once in the DOM.
Rails ships helpers that emit exactly that markup:
<%= everywhere_nav_button "New", new_note_path, icons: { ios: "plus", android: "add" } %>
<%= everywhere_fab new_note_path %>
<%= everywhere_nav_menu "More" do %>
<%= everywhere_menu_item "Delete", note_path(@note), method: :delete, style: "destructive" %>
<% end %>
Plus native_app?, native_platform, everywhere_badge,
everywhere_biometric_lock, and friends.
Commands
| Command | What it does |
|---|---|
every install |
Prepare an app (boot stub, config/everywhere.yml, config tweaks) — --local, --agents |
every doctor |
Check the toolchain — Tebako, Rust, brew deps, Xcode, Android SDK/JDK |
every dev |
Dev server with native shells on demand (--desktop --ios --android --mobile --browser) |
every logs |
Stream shell logs from the Simulator or Android device |
every build |
Package natively — desktop, --ios, or --android |
every icon |
Generate .icns / .ico / Linux icons from a source PNG |
every release |
Sign, notarize, staple, and emit a release.json receipt |
every updates keygen |
Generate a minisign-compatible update-signing keypair |
every publish |
Publish a release to your update bucket |
every clean |
Remove the shell build caches |
every shell-dir |
Print the path of the bundled shell template |
every platform … |
Hosted builds: login, logout, auth status, build |
Run every with no arguments for the full list, or every <command> --help for
a command's flags.
Auto-updates
Desktop releases can update themselves from a bucket you own, with no service in the middle:
every updates keygen # once, per app
every release # build, sign, notarize, staple
every publish --notes-file NOTES.md # sign the manifest, upload, flip latest.json
The public key ships in the app; the secret key never leaves your machine. Apps
can check, install, and switch channels at runtime via Everywhere.updates.
Hosted builds
Don't have a Mac, or want iOS and Android built without babysitting Xcode, the Android SDK, and a signing setup? The RubyEverywhere Platform — a paid service, coming soon — runs the same builds on managed machines and hands back the artifacts. The CLI already speaks to it:
every platform login
every platform build --target ios-arm64,android-arm64
Same gem, same config/everywhere.yml, same output — you're paying for the
build machines and the toolchain upkeep, not for features. The gem stays free
and complete on its own.
Project layout
lib/everywhere/ the CLI, config, builders, and Rails engine
exe/every, exe/rbe the executables
bridge/ @rubyeverywhere/bridge — the JS half
support/desktop/ the Tauri desktop shell template
support/mobile/ios/ the Hotwire Native iOS shell template
support/mobile/android/ the Hotwire Native Android shell template
support/agents/ the AGENTS.md guide `every install --agents` writes
The shell templates have their own READMEs documenting the freeze rules — what the CLI stamps per app and what it must never touch: iOS · Android.
Contributing
Bug reports and pull requests are welcome. Run the test suite with rake test.
License
Released under the MIT License. Copyright © 2026 Andrea Fomera.