Skip to content

Architecture

aaalllexxx edited this page Jun 30, 2026 · 2 revisions

Architecture

ENPAF runs the same app in two environments: a desktop dev server and an on-device Android WebView. The bridge abstracts the transport so your code is identical in both.

        ┌─────────────────────────── Your app ───────────────────────────┐
        │  main.py (Python)                 app/ (HTML/CSS/JS)            │
        │  routes · bridge handlers · events    UI · enpaf.* calls        │
        └───────────────┬─────────────────────────────┬──────────────────┘
                        │                              │
              ┌─────────┴─────────┐          ┌─────────┴──────────┐
              │   DEV (desktop)   │          │  ANDROID (device)  │
              │  Flask + SocketIO │          │  Chaquopy + WebView│
              └─────────┬─────────┘          └─────────┬──────────┘
                        │  WebSocket / HTTP             │  JS interface
                        └──────────────┬───────────────┘
                               Python ↔ JS Bridge

Core components

Component File Role
EnpafApp core/app.py Facade tying everything together; detects Android; wires modules.
Bridge core/bridge.py Python↔JS calls + event push; SocketIO (dev) or WebView (Android).
EventEmitter core/events.py Thread-safe pub/sub.
Router core/router.py Routes + Jinja2 rendering (dev); lazy-imports Jinja2.
Storage core/storage.py SQLite key/value + collections.
DeviceAPI core/api.py Sensors, permissions, toasts, NFC, … (Android + dev stubs).
Capability modules android/*.py wifi, bluetooth, location, sensors, nfc, audio, battery, notifications, device, permissions, media, biometric.
DevServer core/server.py Flask + Flask-SocketIO dev server.
AndroidRuntime android/runtime.py On-device startup.
APKBuilder / ProjectGenerator builder/*.py Generate + build the Gradle/Chaquopy project.

Dev mode

app.run() on desktop starts DevServer (Flask + Socket.IO). It:

  • Serves your app/ folder.
  • Injects /enpaf-bridge/enpaf.js into the <head>.
  • Bridges JS calls over a WebSocket, with an HTTP fallback (POST /enpaf-api/bridge-call) when Socket.IO isn't available.
  • Exposes a settings/inspector panel at /enpaf-settings.

This is why you can build and preview the entire app — including bridge handlers and device stubs — without a phone.

Dev-server security

The dev server is meant for localhost, and is hardened accordingly:

  • Socket.IO CORS is locked to same-origin on loopback binds (http://localhost:<port> / http://127.0.0.1:<port>), so a random web page you have open can't open a WebSocket to the server and drive the bridge or storage (cross-site WebSocket hijacking). If you explicitly bind to a non-loopback host you've opted into exposure, so CORS falls back to * with a warning — set a real ENPAF_SECRET_KEY and only do this on a trusted network.
  • Static file serving is guarded against path traversal — requested paths are resolved and must stay within the app/ directory (the HTML branch can't escape it).
  • The Flask SECRET_KEY is overridable via the ENPAF_SECRET_KEY environment variable.

These only affect the desktop dev server; on-device there is no HTTP server (the WebView loads local assets and talks to Python through the JS interface).

Android mode

The builder generates a Gradle project where Chaquopy embeds CPython. A Java MainActivity hosts a full-screen WebView that loads your UI from file:///android_asset/www. On startup, MainActivity:

  1. Sets ENPAF_ANDROID=1 and ENPAF_DATA_DIR=getFilesDir().
  2. Imports the Python main module (running your main.py).
  3. Hands the Activity + WebView to Python (app._attach_android).

The bridge then uses a JavaScript interface (window.EnpafAndroidBridge) for JS→Python calls, and evaluates window.__enpaf_event(...) / __enpaf_callback(...) in the WebView for Python→JS pushes.

Capability dispatch

JS reaches Python capabilities through two gateways, both allow-listed:

  • enpaf.api(method, args)__enpaf_apiDeviceAPI.invoke(method, args).
  • enpaf.mod(module, method, args)__enpaf_modapp.<module>.invoke(method, args).

Each module validates the method name against an _ALLOWED set, so JS can only call intended methods — never arbitrary Python attributes.

Native callbacks

Chaquopy's dynamic_proxy can implement Java interfaces but not subclass abstract classes. So native callbacks for abstract classes are implemented in MainActivity (Java) and forwarded to Python:

  • BroadcastReceiver → a generic EnpafBroadcastReceiver + registerEnpafReceiver(tag, actions)app._on_broadcast_receive(tag, intent) (used by Wi-Fi/Bluetooth scanning).
  • BiometricPrompt.AuthenticationCallbackMainActivity.authenticateBiometric()app._on_biometric_result(ok, err) → the biometric_result event.

MainActivity extends Activity (not FragmentActivity), so the biometric prompt uses the framework android.hardware.biometrics.BiometricPrompt (API 28+).

Runtime constraints

Two rules keep on-device apps from crashing on launch — important if you hack on the framework or bundle extra Python:

  1. No top-level third-party imports in bundled modules. Chaquopy only ships the stdlib plus whatever python_requirements adds. A module-level import jinja2/flask/… in any bundled module → ModuleNotFoundError → instant crash. Keep such imports lazy (inside the function that needs them). That's why router.py imports Jinja2 only inside render().

  2. Writable data must use ENPAF_DATA_DIR. The app's sources are packaged read-only; writing next to them crashes. Storage writes to ENPAF_DATA_DIR (the app's private files dir on-device), set by MainActivity.

Android detection avoids import android (which Chaquopy can't resolve as a bare module) and instead checks the ENPAF_ANDROID env var.

Build pipeline

enpaf.json + main.py + app/  ──►  ProjectGenerator  ──►  Gradle project
                                   (manifest, MainActivity, assets, Chaquopy)
                                          │
                                          ▼
                                   Gradle (AGP 8.2, Gradle 8.4)
                                   + Chaquopy 15.0.1  ──►  signed APK ──► dist/

See Building APKs and Release & Signing.

Clone this wiki locally