The high-level architecture is composed of four logical layers and the connections between them:
-
Web UI (React frontend)
- React
- TanStack Query
- Socket.IO client
- WebSocket to maintain frida session state in realtime
- shadcn UI
- tailwindcss
- Base UI
-
Backend Server
- Hono for REST API
- Socket.IO for real-time API
- Drizzle ORM with SQLite for data persistence
- Acts as the Frida session manager and the bridge between the frontend and agent
-
Frida Session / Backend Process
- A Frida session is created per client connection and the platform-specific agent script is loaded into the target process
- RPC surface exposed by the agent follows the pattern
script.exports.invoke(namespace, method, args) - The web server proxies RPC calls, streams logs, and persists hook/trace data into stores
-
Target Device (Frida Agent injected into app)
- Agent runs inside the target app (iOS / Android) and provides:
- Registry for module routing
- Modules for inspection and control
- Hooks for runtime interception
- Bridges for language/runtime interop
- Communicates with the backend over the Frida transport channel
- Agent runs inside the target app (iOS / Android) and provides:
- agent/ — Frida agent in TypeScript, compiled per platform. iOS (fruity) and Android (droid) agents, compiled outputs (dist/) and generated types/ used by the frontend.
- gui/ — React web frontend (Vite, Tailwind, shadcn/ui). Main code under gui/src with components, hooks, RPC client.
- src/ — Backend server (Hono + Socket.IO)
- app.ts: REST API
- ws.ts: Socket.IO server and session management
- drizzle/ — Database migration files (auto-generated by drizzle-kit)
- scripts/ — Build and development helper scripts
Two namespaces handle real-time communication:
/devices— Broadcasts device list changes to all connected clients/session— Manages individual Frida sessions (one per connected client)
Session lifecycle:
- Client connects with query params (
device,platform,mode,bundle/pid) - Server attaches to the Frida device and resolves the target PID
- A Frida session is created and the platform-specific agent script is loaded
- Data stores (HookStore, CryptoStore, etc.) are initialized per session
- The
readyevent is emitted with the PID - RPC calls, logs, and hook events are proxied between client and agent
- Pin snapshots are persisted after changes
Each store class follows a consistent pattern: append(), query(), count(), rm(). All stores use a composite key of (deviceId, identifier) for session isolation.
| Store | Backing | Purpose |
|---|---|---|
HookStore |
SQLite | ObjC/native method hook logs |
CryptoStore |
SQLite | Cryptographic API interception logs |
NSURLStore |
SQLite + files | HTTP/HTTPS request logs with body attachments |
HttpStore |
SQLite + files | Android HTTP/WebSocket request logs with body attachments |
FlutterStore |
SQLite | Flutter method channel events |
JNIStore |
SQLite | Android JNI call traces |
XPCStore |
SQLite | iOS XPC/NSXPC message traces |
PrivacyStore |
SQLite | Sensitive API access logs |
HermesStore |
SQLite | Captured Hermes bytecode blobs |
PinStore |
File system | Hook rule snapshots for persistence |
LogWriter |
File system | Syslog and agent log streaming |
Due to the limitation of web UI, there is no project or workspace concept, all data is stored in a global directory and disambiguated by device ID and target identifier (bundle or PID).
Uses Drizzle ORM with Node.js node:sqlite for SQLite persistence.
Migrations live in drizzle/ and are auto-generated by drizzle-kit.
The SessionContext manages the active instrumentation session:
- Current platform (
fruity/droid) - Device ID, bundle/PID, mode
- Socket.IO connection lifecycle
- Typed RPC proxies that are platform-aware (accessing the wrong platform's API throws)
The frontend creates a proxy-based RPC client (gui/src/lib/rpc.ts) that:
- Queues calls until the
readyevent fires - Provides namespace.method(...args) syntax via
Proxy - Returns typed promises matching the agent's exported interfaces
- Rejects pending calls on disconnect
TanStack Query hooks (useFruityQuery, useDroidQuery, and usePlatformQuery) wrap RPC calls with caching, refetching, and error handling.
Two independent agents compiled by frida-compile:
fruity(iOS) — Usesfrida-objc-bridgeandfrida-swift-bridgedroid(Android) — Usesfrida-java-bridge
Each agent exports four RPC methods:
rpc.exports = {
invoke(namespace, method, args) // Dynamic method dispatch
interfaces() // List all available namespace.method pairs
restore(rules) // Restore previously saved hook rules
snapshot() // Capture current hook state
}Each platform defines a static router (router.ts) mapping namespace names to module objects:
// fruity/router.ts
export default { checksec, classdump, cookies, crypto, fs, keychain, ... }The createRegistry(route) function (in common/registry.ts) wraps the router to produce invoke() and interfaces() functions. When invoke("fs", "ls", ["/"]) is called, it looks up route.fs.ls and calls it with the arguments.
frida-compile compiles TypeScript to Frida-compatible JavaScript:
dist/fruity.js— iOS agentdist/droid.js— Android agentdist/transport.js— File transfer helper
Type definitions are generated via tsgo and output to agent/types/ for use by the frontend (aliased as @agent in Vite config).
Bridge scripts (Java, ObjC, Swift) are downloaded from PyPI packages and stored in dist/bridges/.
The scripts/build-sea.ts script implements the Node.js single executable application flow:
- Fetches the radare2 WASM asset
- Bundles the server and its JavaScript dependencies into one CommonJS entry
- Embeds the GUI, agent, Drizzle migrations, skills, radare2 WASM, and native addons as SEA assets
- Builds the executable directly with Node.js 26
At runtime, the SEA entry extracts embedded files and native addons to a content-addressed temporary directory before starting the server. Release CI builds Linux x64, Windows x64, and macOS arm64 on native runners.
tsdown bundles the backend into ESM output (dist/index.mjs). The package includes pre-built agent and GUI assets.