Platform Architecture

Inside ShadowPhone

A comprehensive look at the integrated SaaS ecosystem that powers Instagram automation at scale -- from the Railway cloud brain to the Electron desktop executor, WebSocket modules, Droid Run preflight bridge, and on-demand cloud emulators. Built for GrapheneOS Pixel phones, with most modules portable to standard Android emulators.

Three-Layer Architecture

The core design principle is separation of intelligence from execution. All smart automation logic lives server-side and is never distributed in the desktop binary.

Cloud Layer

"The Brain"
Next.js App

Vercel -- Dashboard, APIs, Auth, Billing, Blog

Railway (Python)

57+ automation modules, screen analysis, action planning

Supabase

Postgres + RLS -- accounts, devices, stats, subscriptions

Transport Layer

WebSocket + IPC
WebSocket Bridge

Persistent WS to Railway for real-time command streaming

Electron IPC

Context bridge between Next.js renderer and main process

Local Executor

"The Dumb Client"
Electron Desktop

80+ IPC methods, device management, content pipeline

ADB Bridge

tap / swipe / type / screenshot / UI dump commands

Devices

Pixel phones (GrapheneOS, multi-profile) or cloud emulators via ADB-over-TCP (no profile switching)

WebSocket Module System

The WebSocket layer is the beating heart of ShadowPhone's real-time automation. When a user clicks Run on any of the 57+ automation modules, the execution follows a tightly orchestrated path that keeps all intelligence server-side.

Execution Flow

  1. 1User triggers a module from the desktop dashboard UI.
  2. 2ModuleRunner (renderer) fetches a 7-day HS256 JWT from /api/module-token.
  3. 3Config is sent to Electron via window.electronAPI.runModuleWs().
  4. 4Electron opens a persistent WSS connection to the Railway Python server at /ws/execute.
  5. 5The server’s WS_MODULE_HANDLERS dispatch picks the correct Python handler.
  6. 6RemoteDevice API sends ADB commands back through the socket as JSON payloads (tap, swipe, input, find, wait, etc.).
  7. 7The Electron client executes each command via ADB and returns UI XML screen dumps for server-side analysis.
  8. 8Real-time progress, logs, and user prompts (e.g. 2FA codes) stream back to the dashboard.

The critical design constraint is Triple Sync: every module ID must be registered in four synchronized locations -- modules.json (UI), module-runner.ts (whitelist), ws-module-client.js (Electron bridge), and server.py (Python handler). Missing any one causes silent failures.

Module categories span Instagram (posting, engagement, DMs, stories, reposting), account management (creation, login, validation, profile switching), content ops (media push, gallery cleanup, fingerprinting), and workflow utilities (delay, conditional, notification). Config-driven forms are generated from module metadata, so adding a new module requires no UI code changes.

Electron Desktop Dashboard

The Electron 40 desktop app is the operator's command center. It loads the Next.js web application in a BrowserWindow while running a powerful main process that bridges the UI to physical Android devices via ADB.

Main Process (electron/main.js)

  • ~4,000 lines orchestrating IPC handlers, device state, and module execution.
  • Manages ADB process lifecycle and device discovery polling.
  • Holds running module state and abort controls.
  • Runtime queue relay that claims planned MCP jobs and executes them locally.
  • Auto-update distribution via electron-updater + Supabase app_releases table.

Preload Bridge (electron/preload.js)

  • 80+ IPC methods securely exposed via contextBridge.
  • Device management: getDevices, refreshDevices, adbConnect/Disconnect.
  • Input control: takeScreenshot, executeTap, executeSwipe, inputText.
  • Module execution: runModule, runModuleWs, abortModule, onModuleProgress.
  • Content pipeline: createContentFolders, pushContentToPhone.

The renderer UI lives in components/desktop/ and includes the full operator experience: module launcher, QuickRun workflow builder, profiles & accounts manager, analytics dashboard, AI batch generator, content tools, and settings. Each component communicates through window.electronAPI to trigger real device actions without any direct ADB access from the browser context.

Railway Cloud Brain

Railway hosts the Python FastAPI server that contains all automation intelligence. This is the deliberate "moat" of the product: the distributed Electron app is a dumb executor, while every decision about what to tap, when to wait, how to navigate Instagram's UI, and how to respond to edge cases lives exclusively in the Railway-hosted server.

Server Responsibilities

  • WS_MODULE_HANDLERS registry maps module IDs to async Python handler functions.
  • RemoteDevice API parses screen XML dumps and decides the next action sequence.
  • Dual auth verification: Clerk RS256 JWTs for identity + HS256 Module Tokens for long-running sessions.
  • Module action planner generates executable step sequences returned via REST for queue-based execution.
  • Popup handler and miscellaneous popup handler for Instagram modal/alert interception.
  • Stats scraper, content selectors, and account validators for automated health checks.

The server auto-deploys from the railway/ directory on main branch pushes. Production fallback URLs are shadowphone2-production.up.railway.app and shadowphone-production.up.railway.app, ensuring the desktop app always finds a working endpoint even if environment configuration is missing.

Droid Run Bridge

Droid Run is an open-source Android device automation framework. ShadowPhone does not use it as the primary automation engine -- instead it integrates the Droid Run Portal as a preflight and health layer. Before any automation module fires (posting, engagement, DMs), the bridge verifies the target device is in a known-good state, configures reverse WebSocket connections, and enables local APIs.

Preflight Sequence

  1. 1QuickRun inserts a droidrun_shadowphone_bootstrap step at the start of every workflow.
  2. 2The bootstrap handler pings the Portal agent on the device via content://com.droidrun.portal/ping.
  3. 3If healthy, it reads Portal state, retrieves an auth token, and configures reverse WebSocket transport.
  4. 4The local HTTP+WS APIs on the device are enabled so subsequent modules can communicate directly.
  5. 5Same-profile skip optimization: if the Portal is already configured for the current profile, the preflight exits early.

Bridge Module IDs

  • droidrun_shadowphone_bootstrap -- full preflight setup.
  • droidrun_portal_ping -- verify Portal health on-device.
  • droidrun_portal_state -- read Portal state snapshots.
  • droidrun_portal_auth_token -- retrieve auth token.
  • droidrun_portal_configure_reverse -- set up reverse WS.
  • droidrun_portal_enable_local_api -- enable local HTTP+WS APIs.

Vendor Sources

The vendored Droid Run and Droid Run Portal sources live under electron/vendors/ and are synced from upstream mirrors via rsync. WS runtime handlers in electron/python/server.py execute content provider commands against content://com.droidrun.portal/* endpoints on the device.

Because Portal communicates over standard ADB content providers, the preflight works on any Android target that has the Portal APK sideloaded -- including cloud emulators.

Cloud Devices & Pop-Up Emulators

ShadowPhone was built for physical Pixel phones running GrapheneOS, but the platform now includes a full cloud-device control plane that treats remote Android instances (VPS emulators, Genymotion Cloud, Droid Run Cloud, and Mobilerun) as first-class citizens alongside USB devices.

GrapheneOS vs Cloud Emulator Compatibility

The automation stack has two distinct layers. Everything that uses standard Android primitives (Appium + UiAutomator2 over ADB) works identically on emulators. The only exception is the GrapheneOS-exclusive multi-profile system.

ModuleGrapheneOSCloud Emulator
Engagement (Like / Save / Comment)YY
Follow / UnfollowYY
Story ViewerYY
DM SendingYY
Post Content (Photos / Reels / Stories)YY
QuickRun WorkflowsYY
Droid Run PreflightYY
ProtonVPN ModuleYY
Airplane Mode ControlYY
Profile Switching + IP RotationYN

Why profile switching fails on emulators: GrapheneProfileSwitcher calls am switch-user, pm create-user --profileOf 0, and pm rename-user -- commands that GrapheneOS exposes with fewer restrictions than stock AOSP. Stock Android and emulators limit multi-user to managed profiles only, and org.grapheneos.setupwizard does not exist. On emulators, each account requires its own emulator instance instead of a separate on-device profile.

How Cloud Devices Are Managed

  • Cloud devices are stored in the same devices table with source_type='cloud', so plan limits and ownership checks apply identically.
  • Provider presets (Budget VPS, Droidrun Cloud, Genymotion) streamline registration with pre-filled configuration.
  • API routes at /api/cloud/devices support full CRUD with plan-gated limits.
  • The desktop app bridges ADB over TCP: users enter a host:port target and the app runs adb connect to attach the remote emulator as if it were local.
  • Heartbeat and last_seen tracking keep cloud device status current.

Provider Adapter Layer

The runtime provider system (lib/runtime/providers/) defines an adapter contract for provisioning, syncing, starting, stopping, and terminating cloud instances. Currently two adapters are implemented: Local ADB (for connected physical or network devices) and Mobilerun (managed cloud Android API). Per-user provider credentials are stored server-side with AES encryption, with the key derived from RUNTIME_PROVIDER_CREDENTIAL_KEY or server secrets as a safe fallback.

The QuickRun UI natively supports both execution modes: desktop (local relay) and cloud_worker (queue-based execution with cloud device picker). When cloud mode is selected, each workflow step is queued via /api/runtime/jobs, waits for terminal states, and can cancel pending steps if the run is stopped. This makes "pop-up emulators" operational -- users can spin up an Android instance, register it, connect over ADB TCP, and run the full automation suite except profile switching.

Runtime Jobs & Queue System

The runtime jobs API provides a queue-first execution model backed by the module_runs table. This system powers both desktop relay execution and headless cloud worker execution.

Job Creation

POST /api/runtime/jobs creates a planned run record, resolves the target device, enforces action quotas, issues a runtime JWT, and fetches the module action plan from Railway.

Job Execution

Desktop relay polls /api/runtime/queue/next to claim jobs. Cloud workers claim cloud_worker mode jobs. Both update status, progress, and heartbeat through the same API surface.

SSE Streaming

GET /api/runtime/jobs/:id/events provides a Server-Sent Events stream for real-time progress monitoring, enabling external clients to watch automation runs as they happen.

Module runs carry execution metadata including execution_mode (desktop vs cloud_worker), runner_id, lease_expires_at, and execution_target (device details as JSONB). The desktop relay attempts both claim modes as a fallback, allowing cloud-queued jobs to execute from the desktop app when no dedicated worker is deployed.

MCP Server & Programmatic Access

ShadowPhone exposes a Model Context Protocol (MCP) server that allows AI agents and external automation tools to interact with the platform programmatically. The server runs as a standalone Node.js process and communicates over stdio using the MCP protocol.

Available MCP Tools

user_list_cloud_devices

List registered cloud devices.

user_upsert_cloud_device

Register or update a cloud device.

user_set_cloud_device_status

Change device status.

user_list_runtime_modules

List available automation modules.

user_start_droidrun_run

Start a single Droidrun bridge module.

user_start_droidrun_batch

Batch-execute multiple bridge modules.

user_start_module_run

Queue any module as a runtime job.

Authentication uses scoped API keys with rate limiting (90 calls per 60s window). Keys are hashed with SHA-256 and stored in Supabase. Valid scopes include runtime:read, runtime:write, devices:read, accounts:write, and more -- enabling fine-grained access control for third-party integrations.

Security & Data Architecture

Security is layered across every component of the stack:

Authentication

  • Clerk handles all user auth with RS256 JWTs.
  • Clerk webhooks sync profiles to the Supabase profiles table.
  • Module Tokens (HS256, 7-day TTL) are issued for long-running desktop sessions.
  • Railway verifies both token types via dual-verification.

Data Protection

  • Supabase Row Level Security on all user-facing tables.
  • Instagram credentials are encrypted at rest.
  • Provider credentials use AES encryption with a dedicated key.
  • Content stays local -- media never leaves the user's machine.
  • MCP API keys are hashed with SHA-256 + pepper.

The Supabase data model spans 32+ tables organized by subsystem: device-related tables (devices, device_profiles), account tables (instagram_accounts), billing (subscriptions, plan_tiers), execution tracking (module_runs, daily_stats), and analytics (ig_accounts, ig_account_stats). Each subsystem maintains its own table family with intentional isolation.

Technology Stack

Frontend

Next.js 16, React 19, TypeScript, Tailwind 4

Desktop

Electron 40, ADB, IPC Handlers

Backend

Python FastAPI on Railway

Database

Supabase PostgreSQL + RLS

Auth

Clerk (RS256 JWTs)

Payments

PayPal Subscriptions

UI Kit

Radix UI + Shadcn, Framer Motion

Hosting

Vercel (web) + Railway (brain)

AI / Media

Private AI Backend, Remotion