I wanted to fix a bug on the train. I didn't want to just read the code or leave myself a note. I wanted to open the project, change the line, run the tests and push. My phone couldn't do that, so I built an app that could.
It's called Tide. It's a code editor, terminal and git client for iPhone and iPad, and it runs your code on the device, offline. It's free, with no subscription and no account.
This post covers how it works. Getting real runtimes onto iOS turned out to be the interesting part.
The problem: iOS doesn't let apps start processes
On a laptop, an IDE runs node, python or git as child processes. iOS apps can't spawn processes at all. So every runtime has to live inside the app, and the terminal can't just hand commands to a shell.
This is the stack I ended up with.
The app shell
- Expo / React Native (New Architecture) for the UI
- CodeMirror 6 for the editor, running in a WebView and built with Vite into a single HTML file, so it works with no network
- xterm.js for the terminal, in its own WebView
There's no real shell, so the terminal runs a small built-in one. It has ls, cd, cat, mkdir, rm and friends, plus node, python, npm, pip, git and sqlite3 as built-ins. It can also run npm run and npm test, as long as the script only uses commands it knows, joined with && or ;.
Node.js: one worker thread per run
Node comes from nodejs-mobile (Node 18). A supervisor script starts with the app, and each program you run gets its own worker thread. So Ctrl-C can stop one script without restarting Node, and each run gets its own heap limit.
There's a catch. Worker threads share one process, and one process has one working directory. If a background service and your script start at the same time, a process.chdir in one moves the other. I fixed it by giving each worker its own virtual cwd. process.cwd(), chdir, relative paths in fs, and child_process defaults all resolve against the run's folder.
Two other things caught me out:
- nodejs-mobile's Node is built without
Intl, soIntland regex Unicode property escapes (\p{...}) fail. Express 5 won't run, so the template uses Express 4. - There's no WebAssembly on iOS here. ssh2 ships a WASM poly1305, so the bundle swaps it for a stub and turns that cipher off.
Python: embedded CPython
Python is an embedded CPython (Chaquopy on Android). Each run gets a thread. input() is wired to the terminal, and Ctrl-C raises KeyboardInterrupt in the script.
Every run shares one interpreter, which makes running two programs at once tricky. sys.stdout, os.environ, the cwd and even __main__ are process-wide. Tide routes those per run, so a dev server and a script in the same project don't trip over each other.
Ruby and PHP: WebAssembly in a hidden WebView
Neither Hermes nor nodejs-mobile has WebAssembly on iOS, but WKWebView does. So Ruby (ruby.wasm) and PHP (php-wasm) run in a hidden WebView. Each run copies the project into the interpreter's virtual disk and writes back whatever changed. Lua runs through fengari, which is plain JavaScript.
Packages without native builds
npm install and pip install are implemented in the Node supervisor:
- npm resolves versions from the registry, checks each tarball's integrity and extracts into
node_modules. Install scripts never run. - pip resolves dependencies, extras and environment markers, checks each wheel's sha256 and installs into a
.pylibsfolder that's onsys.path.
Anything that needs a native build can't install on the device, and I'd rather say that up front. For those cases there's an optional Remote mode: a small server you host that runs one Docker container per project and syncs files both ways.
Git: isomorphic-git, plus the safety real git has
Git is isomorphic-git, bundled into the Node side. Out of the box it will happily check out over your uncommitted changes, so I added the checks real git does. checkout, pull, rm and stash now refuse to overwrite local work. Getting merges, symlinks and submodules to behave like real git took a lot longer than I expected.
There's also GitHub, GitLab and Bitbucket support for pull requests, issues and Actions.
Bring-your-own AI
Tide has an AI agent, inline completions and "Fix with AI", but it uses your own key. That can be Anthropic, OpenAI, Gemini or OpenRouter, or a local Ollama or LM Studio server. Keys stay in the Keychain and requests go straight to the provider.
The agent stages its edits as diffs for you to review, and asks before running any command. Staged changes are saved to disk before anything is written, so if iOS kills the app mid-change you can still reject it.
What I learned
Mobile constraints shape everything. No processes, no WebAssembly in the JS engine and no Intl each forced a different design.
"Works like real git" is a high bar. Most of my git bugs came from the library being more permissive than git.
Concurrency inside one process was the hardest part. Every global (cwd, env, stdout) needed a per-run answer.
Try it
Tide is free on the App Store for iPhone and iPad:
https://apps.apple.com/in/app/tide-code-editor/id6828353185
If you've wanted something specific from a mobile IDE, tell me in the comments. I'm also happy to go deeper on any part of the stack.
Top comments (0)