Skip to content

Commit 764d012

Browse files
pivot: drop Flutter scaffold, retarget for native Kotlin Android
Research showed nodejs-mobile-flutter doesn't exist as a maintained dependency (JaneaSystems repo is 404, official org ships only RN + Cordova plugins, pub.dev has nothing). On closer inspection the plugin layer is dead weight for a WebView-shell architecture — every plugin exists to bridge messages between an outer JS runtime and Node, but our "bridge" is just HTTP between the WebView and Fastify. Decision discussed in JSS#366. This commit removes the Flutter scaffold (pubspec.yaml, lib/, test/, the Flutter-shaped android/) and updates README + ARCHITECTURE to describe the native Kotlin direction: MainActivity hosting a WebView, libnode.so linked via JNI/CMake (~80 LOC C++ from JaneaSystems samples), foreground service wrapping the Node thread. scripts/bundle-jss.sh retargeted from assets/nodejs-project/ to app/src/main/assets/jss/ to match the standard Android assets path. Stays the same: the verified launch args, the bundle script's contract (npm pack + prod install + sanity check, refuses native modules), the repo's gh-pages trunk.
1 parent 2f03e58 commit 764d012

28 files changed

Lines changed: 170 additions & 866 deletions

.gitignore

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,42 @@
1-
# Flutter / Dart
2-
.dart_tool/
3-
.flutter-plugins
4-
.flutter-plugins-dependencies
5-
.packages
6-
.pub-cache/
7-
.pub/
1+
# Gradle
2+
.gradle/
83
build/
9-
.metadata
4+
out/
105

11-
# IDE
6+
# Android Studio / IntelliJ
127
.idea/
13-
.vscode/
148
*.iml
159
*.ipr
1610
*.iws
17-
.DS_Store
11+
local.properties
12+
captures/
1813

19-
# Android
20-
android/.gradle/
21-
android/captures/
22-
android/local.properties
23-
android/app/release/
24-
android/app/debug/
14+
# Build artifacts
15+
*.apk
16+
*.aab
17+
*.ap_
18+
*.dex
19+
*.class
20+
proguard/
21+
22+
# Signing keys (NEVER commit)
2523
*.keystore
2624
*.jks
27-
android/key.properties
25+
key.properties
26+
27+
# Logs
28+
*.log
2829

29-
# iOS (kept in case we ever scope iOS in)
30-
ios/Pods/
31-
ios/.symlinks/
32-
ios/Flutter/Flutter.framework
33-
ios/Flutter/Flutter.podspec
30+
# OS / editor
31+
.DS_Store
32+
.vscode/
3433

35-
# Bundled JSS — copied in by scripts/bundle-jss.sh, not tracked
36-
assets/nodejs-project/
34+
# Bundled JSS (produced by scripts/bundle-jss.sh, not tracked)
35+
app/src/main/assets/jss/
3736

38-
# Node (only present transiently while bundling)
37+
# Vendored libnode binaries (produced by scripts/fetch-libnode.sh, not tracked)
38+
app/libnode/
39+
40+
# Node (transient while bundling)
3941
node_modules/
4042
npm-debug.log
41-
yarn-debug.log
42-
yarn-error.log
43-
44-
# Generated
45-
.flutter-version
46-
# Note: pubspec.lock IS committed (Flutter convention for application packages)

ARCHITECTURE.md

Lines changed: 74 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,66 +2,81 @@
22

33
## Goal
44

5-
Ship a single-tap Android install that runs a personal Solid pod on the phone. The user opens the app and a JSS instance is running at `http://localhost:4443/` inside the app's process; closing the app stops the pod (or, with the foreground service, keeps it alive in the background).
5+
Ship a single-tap Android install that runs a personal Solid pod on the phone. The user opens the app and a JSS instance is running at `http://127.0.0.1:4443/` inside the app's process; closing the app stops the pod (or, with the foreground service, keeps it alive in the background).
6+
7+
## Why native Kotlin (not Flutter, not React Native, not a plugin)
8+
9+
> The original draft specified Flutter. Research showed there's no maintained Flutter↔nodejs-mobile plugin, and on closer inspection **the plugin layer is dead weight for our shape**: every plugin (RN, Flutter, Capacitor) exists to marshal messages between an outer JS runtime and the embedded Node. Our UI is a `WebView`, the WebView talks to JSS over HTTP, and the bridge is HTTP. Pivot rationale: [JSS#366 comment](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/366#issuecomment-4401796552). Same APK story (JS bundle + WebView), one fewer abstraction.
610
711
## Components
812

913
### 1. Bundled JSS (the asset)
1014

11-
- Path inside APK: `assets/nodejs-project/`
15+
- Path inside APK: `app/src/main/assets/jss/`
1216
- Contents: full JSS tree as published to npm — `bin/`, `src/`, `package.json`, `node_modules/`
1317
- Why the full `node_modules/`: it's pure JS, so copying it once at build-time is cheaper than `npm install` on first launch (and avoids needing a network on first run)
14-
- Size estimate: ~1.4 MB unpacked (per `npm publish` output for `javascript-solid-server@0.0.169`) + node_modules ≈ 30–50 MB
18+
- Verified bundle size for `javascript-solid-server@0.0.169`: **~62 MB / 5444 files** (per `scripts/bundle-jss.sh` smoke run)
19+
- Not tracked in git; produced fresh from `scripts/bundle-jss.sh` for each build
1520

16-
### 2. nodejs-mobile runtime
21+
### 2. nodejs-mobile runtime (`libnode.so`)
1722

18-
- Provides a patched Node binary cross-compiled for Android (arm64-v8a, armeabi-v7a, x86_64)
19-
- Runs as a dedicated thread inside the Android process
20-
- Restrictions vs upstream Node: no `child_process`, no `cluster`, no `worker_threads` until recent versions, no native addons unless explicitly built for Android
23+
- Patched Node binary cross-compiled for Android (arm64-v8a, armeabi-v7a, x86_64)
24+
- Vendored from [`nodejs-mobile/nodejs-mobile` releases](https://github.com/nodejs-mobile/nodejs-mobile/releases) by `scripts/fetch-libnode.sh`
25+
- Currently ships **Node 18.20.4** (last release Oct 2024; Node 18 EOL April 2025 — see Risks)
26+
- Restrictions vs upstream Node: no `child_process`, no `cluster`, no native addons unless explicitly built for Android
27+
- Linked via JNI/CMake; loaded into a dedicated thread inside the Android process
2128

22-
### 3. Flutter plugin (`nodejs-mobile-flutter`)
29+
### 3. JNI shim (`native-lib.cpp`)
2330

24-
- Boots the Node runtime, hands it the entry script (`assets/nodejs-project/bin/jss.js`) plus startup args
25-
- Provides a Dart channel for bidirectional message passing (used post-MVP for native UI ↔ JSS commands; v1 doesn't need it)
31+
- ~80 lines of C++, lifted near-verbatim from [JaneaSystems/nodejs-mobile-samples](https://github.com/JaneaSystems/nodejs-mobile-samples/tree/master/android/native-gradle/app/src/main/cpp)
32+
- Two responsibilities:
33+
1. `extern "C" int callintoNode(int argc, char *argv[]) { return node::Start(argc, argv); }` — exposes Node's main loop
34+
2. Pipe stdout/stderr to logcat (otherwise Fastify's startup logs vanish)
35+
- Optional reuse from `nodejs-mobile-react-native`'s C++ layer (MIT licensed): the assetCopy routine and JNI thread setup. We're a server, not a client of `rn-bridge.cpp`'s message channel — we don't need that file.
2636

27-
### 4. Flutter UI
37+
### 4. MainActivity (Kotlin)
2838

29-
- **v1**: a single `WebView` widget pointed at `http://localhost:4443/`
30-
- Initial route depends on JSS config; default lands on the IDP landing or the `/{pod}/` root
31-
- v2: native screens for sign-in, file browse, sharing — replacing the WebView screen by screen
39+
- Single-screen `WebView` pointed at `http://127.0.0.1:4443/`
40+
- WebView config: `domStorageEnabled = true`, `javaScriptEnabled = true`, allow mixed content for loopback, override `shouldOverrideUrlLoading` so off-pod links open in the system browser (avoids in-WebView OAuth capture)
41+
- Boot waits for the foreground service to report Node ready before loading the URL — show a splash in the meantime
3242

33-
### 5. Android foreground service
43+
### 5. JssService (Kotlin foreground service)
3444

35-
- Standard pattern: when JSS is "running" the app posts a persistent notification ("JSS is running on port 4443")
36-
- Required to avoid the OS killing the Node thread when the app goes to background
37-
- User can stop the pod by tapping the notification's Stop action
45+
- Wraps the Node thread in an Android `Service` with `START_STICKY` and a persistent notification ("JSS pod running on :4443")
46+
- Required: without a foreground service, Android kills the Node thread when the app backgrounds
47+
- Notification has a "Stop" action wired to a graceful Fastify shutdown via a process-internal HTTP call (or sigterm via the JNI side)
48+
- Reference: `node_flutter`'s `NodeService.kt` (~133 lines, MIT) is the cleanest example to lift the lifecycle pattern from
3849

3950
## Boot sequence
4051

41-
1. App `main()` → run Flutter app
42-
2. Flutter `initState` → call `NodejsMobile.startNodeProject()` with:
43-
- script: `bin/jss.js`
44-
- args: `['start', '--single-user', '--port', '4443', '--host', '127.0.0.1', '--root', '<android-files-dir>/data', '--idp']`
45-
- env: `JSS_SINGLE_USER_PASSWORD=<derived-or-prompted>` (only on first launch, to seed the IDP account)
46-
3. Node thread starts; JSS imports its modules; Fastify binds to `127.0.0.1:4443`
47-
4. Flutter listens for the "ready" message on the channel (or polls `GET /` until 200) and shows the WebView
52+
1. App `onCreate` → start `JssService` as a foreground service
53+
2. `JssService` spawns a worker thread → JNI `startNode(args)`:
54+
- script: `<filesDir>/jss/bin/jss.js`
55+
- args: `['start', '--single-user', '--port', '4443', '--host', '127.0.0.1', '--root', '<filesDir>/data', '--idp']`
56+
- env: `JSS_SINGLE_USER_PASSWORD=<derived-or-stored>` (only on first launch, to seed the IDP account)
57+
3. Node imports JSS modules; Fastify binds to `127.0.0.1:4443`
58+
4. Service polls `GET http://127.0.0.1:4443/` (100 ms backoff, max ~5 s) → broadcasts ready
59+
5. `MainActivity` receives ready → loads the WebView URL
4860

49-
## Data layout on device
61+
## Asset layout on device
5062

51-
- `<android-files-dir>/data/` — JSS pod data (passed via `--root <path>` or `DATA_ROOT` env)
52-
- `<android-files-dir>/data/.idp/` — IDP credentials, keys (verified by spike: created on first boot)
53-
- `<android-files-dir>/data/.well-known/` — token store, etc. (when `--idp` is on)
54-
- `<cache-dir>/` — JSS notification queues, ephemeral state
63+
| Path | Purpose |
64+
|---|---|
65+
| `<filesDir>/jss/` | JSS tree, copied from APK assets on first launch (or when build version changes) |
66+
| `<filesDir>/data/` | Pod data: `profile/`, `public/`, `private/`, `inbox/`, `settings/`, `.idp/` |
67+
| `<filesDir>/data/.idp/` | IDP credentials + signing keys (verified by spike: created on first boot) |
68+
| `<cacheDir>/` | Ephemeral state (notification queues, etc.) |
5569

56-
The pod's filesystem layout is unchanged from desktop JSS — we just point `--root` at app-private storage. Verified by booting `jss start --single-user --port 4444 --root /tmp/foo --idp`: full pod tree (profile/, public/, private/, inbox/, settings/, .idp/) is created at the supplied path with nothing leaking to `~/.jss/` or the cwd.
70+
The pod's filesystem layout is unchanged from desktop JSS — we just point `--root` at app-private storage. Verified by booting `jss start --single-user --port 4444 --root /tmp/foo --idp`: full pod tree is created at the supplied path with nothing leaking to `~/.jss/` or the cwd.
5771

58-
> **Spike note (2026-05-08):** the CLI flag is `-r, --root <path>`, not `--data-root` as initially drafted. The architecture docs and any future Dart launcher should use `--root`.
72+
> **CLI flag note (verified 2026-05-08):** the flag is `-r, --root <path>`, not `--data-root`.
5973
6074
## Auth model on a single-user phone pod
6175

6276
- `--single-user` mode: one pod, one WebID, no registration UI
63-
- Default: pod owner is the device user; the IDP login screen still works for cross-app sign-in flows (xlogin, did:nostr signers)
64-
- E2EE composes cleanly: the device-local Nostr key is the pod's WebID and the encryption key (see [JSS#365](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/365))
77+
- Default pod owner is the device user
78+
- The IDP login screen still works for cross-app sign-in flows (xlogin, did:nostr signers via NIP-07 in the WebView)
79+
- E2EE composes cleanly: device-local Nostr key is the pod's WebID and the encryption key (see [JSS#365](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/365))
6580

6681
## Public exposure (optional)
6782

@@ -72,23 +87,31 @@ The pod's filesystem layout is unchanged from desktop JSS — we just point `--r
7287
## What we're not doing
7388

7489
| Choice | Why |
75-
|--------|-----|
76-
| Run Node via Termux instead of nodejs-mobile | Termux requires a separate app + manual setup; defeats "one-click install" |
77-
| Cross-compile native deps for Android | Not needed — JSS already uses pure-JS substitutes everywhere |
78-
| Custom Node fork for additional capabilities | nodejs-mobile is maintained; forking is a long-term burden |
79-
| iOS in v1 | App Store rule 2.5.2 (no executable code download) and embedded-interpreter policy is fuzzy; needs separate research |
80-
| Native Flutter UI in v1 | WebView is faster to ship and reuses JSS's existing IDP / mashlib UIs |
81-
82-
## Open questions
83-
84-
- Does `nodejs-mobile`'s most recent release ship Node ≥ 18? (`engines.node: ">=18.0.0"` is JSS's floor; spike on dev box ran Node 24 with one `oidc-provider` warning about preferring v22 LTS — runtime is *forgiving*, but we should pin the mobile target to whatever Node `nodejs-mobile` ships)
85-
- Memory ceiling on cheap Android devices (1 GB RAM)? `oidc-provider` is the heaviest dep; might need `--idp` off in a "lite" mode
86-
- How do we surface the WebID/pod URL outside the app? (Deep link? Share sheet? A "copy URL" button?)
87-
- Does the foreground-service notification need a stop action wired to a graceful Fastify shutdown? (yes — need to expose this via the plugin channel)
88-
- First-run UX: where do we surface the IDP password? Options: (a) auto-generate, store in Android Keystore, never show; (b) prompt user; (c) skip IDP entirely on phone (signed-in via did:nostr only)
90+
|---|---|
91+
| Run Node via Termux | Termux requires a separate app + manual setup; defeats "one-click install" |
92+
| Cross-compile native deps | Not needed — JSS uses pure-JS substitutes throughout |
93+
| Custom Node fork | `nodejs-mobile` is maintained; forking is a long-term burden |
94+
| Flutter / React Native shell | Both pay for an outer JS runtime to talk to Node — but our UI is a WebView, talking to Node over HTTP. The plugin layer is dead weight |
95+
| Bare/Pear runtime | No `fs` / `http` builtins — would require rewriting JSS's transitive deps against `bare-*` modules |
96+
| iOS in v1 | App Store rule 2.5.2 grey zone on dynamic JS eval; OIDC client registration may trip it |
97+
| Native UI (Compose/Views) in v1 | WebView reuses JSS's existing IDP + mashlib UIs; faster to ship |
98+
99+
## Risks / open questions
100+
101+
- **Node 18 EOL.** Upstream `nodejs-mobile` last released Oct 2024 with Node 18.20.4. Node 18 stopped getting security patches April 2025. For a server running OIDC + TLS, this is real. Mitigations: 127.0.0.1-only bind cuts attack surface; track community efforts toward a Node 20/22 mobile build (watch upstream issues).
102+
- **APK size.** `libnode.so` is ~30 MB per ABI. Ship as AAB with ABI splits (arm64-v8a + armeabi-v7a; drop x86_64 unless emulator support is needed) so Play Store delivers per-device.
103+
- **Port collisions.** 4443 may be taken on the device. Plan: try 4443, then 4444, 4445, … and write the chosen port to a `SharedPreferences` the WebView reads. Or bind to port 0 and read back via a JNI return.
104+
- **Memory on low-end devices.** `oidc-provider` is the heaviest dep; `--no-idp` "lite" mode is the fallback for 1 GB RAM phones.
105+
- **First-run IDP password UX.** Options: (a) auto-generate, store in Android Keystore, never show; (b) prompt user; (c) skip IDP entirely on phone (sign in via did:nostr only). Pick before v1.
106+
- **Foreground service stop action.** Notification "Stop" needs to wire to a graceful Fastify shutdown — sigterm via JNI or an in-process HTTP call to a `/.shutdown` admin endpoint we'd need to add to JSS.
107+
- **WebID URL discovery from outside the app.** How does the user get their pod URL into another app on the phone? Options: deep link, share sheet, a "copy URL" button. Pick before v1.
89108

90109
## Build & release
91110

92-
- Debug builds: `flutter run` from a dev box with a connected device
93-
- Release: `flutter build apk --release` produces a signed APK; `flutter build appbundle` for Play Store
94-
- CI (later): GitHub Actions matrix with `subosito/flutter-action`, building both `--debug` and `--release` artifacts on each tag
111+
- Debug: `./gradlew installDebug` from a dev box with a connected device (ADB)
112+
- Release: `./gradlew bundleRelease` produces a signed AAB for Play Store; `./gradlew assembleRelease` for sideload-friendly APKs
113+
- CI (later): GitHub Actions, JDK 17, Android SDK action, NDK action, build both `--debug` and `--release` AABs on each tag
114+
- Pre-build hooks (orchestrated outside gradle for clarity):
115+
- `scripts/bundle-jss.sh` — populates `app/src/main/assets/jss/`
116+
- `scripts/fetch-libnode.sh` — populates `app/libnode/{abi}/libnode.so`
117+
- Both are idempotent and check version pins; CI invokes them before `./gradlew`

0 commit comments

Comments
 (0)