<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kurubaran Anandhan</title>
    <description>The latest articles on DEV Community by Kurubaran Anandhan (@thisiskurubaran).</description>
    <link>https://dev.to/thisiskurubaran</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4235685%2F67580e2e-2524-40f0-b8b4-254135545fe3.png</url>
      <title>DEV Community: Kurubaran Anandhan</title>
      <link>https://dev.to/thisiskurubaran</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thisiskurubaran"/>
    <language>en</language>
    <item>
      <title>iOS won’t let apps spawn processes, so I put the runtimes inside the app</title>
      <dc:creator>Kurubaran Anandhan</dc:creator>
      <pubDate>Mon, 09 Nov 2026 18:27:47 +0000</pubDate>
      <link>https://dev.to/thisiskurubaran/ios-wont-let-apps-spawn-processes-so-i-put-the-runtimes-inside-the-app-2gi2</link>
      <guid>https://dev.to/thisiskurubaran/ios-wont-let-apps-spawn-processes-so-i-put-the-runtimes-inside-the-app-2gi2</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;It's called &lt;strong&gt;Tide&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;p&gt;This post covers how it works. Getting real runtimes onto iOS turned out to be the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: iOS doesn't let apps start processes
&lt;/h2&gt;

&lt;p&gt;On a laptop, an IDE runs &lt;code&gt;node&lt;/code&gt;, &lt;code&gt;python&lt;/code&gt; or &lt;code&gt;git&lt;/code&gt; as child processes. iOS apps can't spawn processes at all. So every runtime has to live &lt;em&gt;inside&lt;/em&gt; the app, and the terminal can't just hand commands to a shell.&lt;/p&gt;

&lt;p&gt;This is the stack I ended up with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The app shell
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expo / React Native&lt;/strong&gt; (New Architecture) for the UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CodeMirror 6&lt;/strong&gt; for the editor, running in a WebView and built with Vite into a single HTML file, so it works with no network&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;xterm.js&lt;/strong&gt; for the terminal, in its own WebView&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no real shell, so the terminal runs a small built-in one. It has &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cd&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;mkdir&lt;/code&gt;, &lt;code&gt;rm&lt;/code&gt; and friends, plus &lt;code&gt;node&lt;/code&gt;, &lt;code&gt;python&lt;/code&gt;, &lt;code&gt;npm&lt;/code&gt;, &lt;code&gt;pip&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt; and &lt;code&gt;sqlite3&lt;/code&gt; as built-ins. It can also run &lt;code&gt;npm run&lt;/code&gt; and &lt;code&gt;npm test&lt;/code&gt;, as long as the script only uses commands it knows, joined with &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; or &lt;code&gt;;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node.js: one worker thread per run
&lt;/h2&gt;

&lt;p&gt;Node comes from &lt;strong&gt;nodejs-mobile&lt;/strong&gt; (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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;process.chdir&lt;/code&gt; in one moves the other. I fixed it by giving each worker its own virtual cwd. &lt;code&gt;process.cwd()&lt;/code&gt;, &lt;code&gt;chdir&lt;/code&gt;, relative paths in &lt;code&gt;fs&lt;/code&gt;, and &lt;code&gt;child_process&lt;/code&gt; defaults all resolve against the run's folder.&lt;/p&gt;

&lt;p&gt;Two other things caught me out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nodejs-mobile's Node is built &lt;strong&gt;without &lt;code&gt;Intl&lt;/code&gt;&lt;/strong&gt;, so &lt;code&gt;Intl&lt;/code&gt; and regex Unicode property escapes (&lt;code&gt;\p{...}&lt;/code&gt;) fail. Express 5 won't run, so the template uses Express 4.&lt;/li&gt;
&lt;li&gt;There's &lt;strong&gt;no WebAssembly&lt;/strong&gt; on iOS here. ssh2 ships a WASM poly1305, so the bundle swaps it for a stub and turns that cipher off.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Python: embedded CPython
&lt;/h2&gt;

&lt;p&gt;Python is an embedded &lt;strong&gt;CPython&lt;/strong&gt; (Chaquopy on Android). Each run gets a thread. &lt;code&gt;input()&lt;/code&gt; is wired to the terminal, and Ctrl-C raises &lt;code&gt;KeyboardInterrupt&lt;/code&gt; in the script.&lt;/p&gt;

&lt;p&gt;Every run shares one interpreter, which makes running two programs at once tricky. &lt;code&gt;sys.stdout&lt;/code&gt;, &lt;code&gt;os.environ&lt;/code&gt;, the cwd and even &lt;code&gt;__main__&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ruby and PHP: WebAssembly in a hidden WebView
&lt;/h2&gt;

&lt;p&gt;Neither Hermes nor nodejs-mobile has WebAssembly on iOS, but WKWebView does. So Ruby (&lt;code&gt;ruby.wasm&lt;/code&gt;) and PHP (&lt;code&gt;php-wasm&lt;/code&gt;) 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Packages without native builds
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;npm install&lt;/code&gt; and &lt;code&gt;pip install&lt;/code&gt; are implemented in the Node supervisor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm resolves versions from the registry, checks each tarball's integrity and extracts into &lt;code&gt;node_modules&lt;/code&gt;. Install scripts never run.&lt;/li&gt;
&lt;li&gt;pip resolves dependencies, extras and environment markers, checks each wheel's sha256 and installs into a &lt;code&gt;.pylibs&lt;/code&gt; folder that's on &lt;code&gt;sys.path&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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 &lt;strong&gt;Remote mode&lt;/strong&gt;: a small server you host that runs one Docker container per project and syncs files both ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git: isomorphic-git, plus the safety real git has
&lt;/h2&gt;

&lt;p&gt;Git is &lt;strong&gt;isomorphic-git&lt;/strong&gt;, 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. &lt;code&gt;checkout&lt;/code&gt;, &lt;code&gt;pull&lt;/code&gt;, &lt;code&gt;rm&lt;/code&gt; and &lt;code&gt;stash&lt;/code&gt; now refuse to overwrite local work. Getting merges, symlinks and submodules to behave like real git took a lot longer than I expected.&lt;/p&gt;

&lt;p&gt;There's also GitHub, GitLab and Bitbucket support for pull requests, issues and Actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bring-your-own AI
&lt;/h2&gt;

&lt;p&gt;Tide has an AI agent, inline completions and "Fix with AI", but it uses &lt;strong&gt;your own key&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Mobile constraints shape everything. No processes, no WebAssembly in the JS engine and no &lt;code&gt;Intl&lt;/code&gt; each forced a different design.&lt;/p&gt;

&lt;p&gt;"Works like real git" is a high bar. Most of my git bugs came from the library being more permissive than git.&lt;/p&gt;

&lt;p&gt;Concurrency inside one process was the hardest part. Every global (cwd, env, stdout) needed a per-run answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Tide is free on the App Store for iPhone and iPad:&lt;br&gt;
&lt;a href="https://apps.apple.com/in/app/tide-code-editor/id6828353185" rel="noopener noreferrer"&gt;https://apps.apple.com/in/app/tide-code-editor/id6828353185&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ios</category>
      <category>reactnative</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
