Project Architecture

July 21, 2026 · View on GitHub

High-Level Overview

The high-level architecture is composed of four logical layers and the connections between them:

  • Web UI (React frontend)

  • 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

Directory Structure

  • 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

Socket.IO

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:

  1. Client connects with query params (device, platform, mode, bundle/pid)
  2. Server attaches to the Frida device and resolves the target PID
  3. A Frida session is created and the platform-specific agent script is loaded
  4. Data stores (HookStore, CryptoStore, etc.) are initialized per session
  5. The ready event is emitted with the PID
  6. RPC calls, logs, and hook events are proxied between client and agent
  7. Pin snapshots are persisted after changes

Data Stores

Each store class follows a consistent pattern: append(), query(), count(), rm(). All stores use a composite key of (deviceId, identifier) for session isolation.

StoreBackingPurpose
HookStoreSQLiteObjC/native method hook logs
CryptoStoreSQLiteCryptographic API interception logs
NSURLStoreSQLite + filesHTTP/HTTPS request logs with body attachments
HttpStoreSQLite + filesAndroid HTTP/WebSocket request logs with body attachments
FlutterStoreSQLiteFlutter method channel events
JNIStoreSQLiteAndroid JNI call traces
XPCStoreSQLiteiOS XPC/NSXPC message traces
PrivacyStoreSQLiteSensitive API access logs
HermesStoreSQLiteCaptured Hermes bytecode blobs
PinStoreFile systemHook rule snapshots for persistence
LogWriterFile systemSyslog 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).

Database Schema (Drizzle + SQLite)

Uses Drizzle ORM with better-sqlite3 for SQLite persistence.

Migrations live in drizzle/ and are auto-generated by drizzle-kit.

Session Context

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)

RPC Client

The frontend creates a proxy-based RPC client (gui/src/lib/rpc.ts) that:

  • Queues calls until the ready event 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.

Agent

Platform-Specific Agents

Two independent agents compiled by frida-compile:

  • fruity (iOS) — Uses frida-objc-bridge and frida-swift-bridge
  • droid (Android) — Uses frida-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
}

Module Registry Pattern

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.

Build & Asset Pipeline

Agent Build

frida-compile compiles TypeScript to Frida-compatible JavaScript:

  • dist/fruity.js — iOS agent
  • dist/droid.js — Android agent
  • dist/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/.

Node.js SEA Build

The scripts/build-sea.ts script implements the Node.js single executable application flow:

  1. Fetches the radare2 WASM asset
  2. Bundles the server and its JavaScript dependencies into one CommonJS entry
  3. Embeds the GUI, agent, Drizzle migrations, skills, radare2 WASM, and native addons as SEA assets
  4. Generates a Node.js SEA preparation blob and injects it into a copy of the current Node.js executable

At runtime, the SEA entry extracts embedded files and native addons to a content-addressed temporary directory before starting the server. Release CI builds each target on its native runner.

npm Package Build

tsdown bundles the backend into ESM output (dist/index.mjs). The package includes pre-built agent and GUI assets.