You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Copy file name to clipboardExpand all lines: ARCHITECTURE.md
+74-51Lines changed: 74 additions & 51 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,66 +2,81 @@
2
2
3
3
## Goal
4
4
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.
6
10
7
11
## Components
8
12
9
13
### 1. Bundled JSS (the asset)
10
14
11
-
- Path inside APK: `assets/nodejs-project/`
15
+
- Path inside APK: `app/src/main/assets/jss/`
12
16
- Contents: full JSS tree as published to npm — `bin/`, `src/`, `package.json`, `node_modules/`
13
17
- 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)
- Not tracked in git; produced fresh from `scripts/bundle-jss.sh` for each build
15
20
16
-
### 2. nodejs-mobile runtime
21
+
### 2. nodejs-mobile runtime (`libnode.so`)
17
22
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
21
28
22
-
### 3. Flutter plugin (`nodejs-mobile-flutter`)
29
+
### 3. JNI shim (`native-lib.cpp`)
23
30
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.
26
36
27
-
### 4. Flutter UI
37
+
### 4. MainActivity (Kotlin)
28
38
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
32
42
33
-
### 5. Android foreground service
43
+
### 5. JssService (Kotlin foreground service)
34
44
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
- 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
48
60
49
-
## Data layout on device
61
+
## Asset layout on device
50
62
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.) |
55
69
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.
57
71
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`.
59
73
60
74
## Auth model on a single-user phone pod
61
75
62
76
-`--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))
65
80
66
81
## Public exposure (optional)
67
82
@@ -72,23 +87,31 @@ The pod's filesystem layout is unchanged from desktop JSS — we just point `--r
72
87
## What we're not doing
73
88
74
89
| 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.
89
108
90
109
## Build & release
91
110
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):
0 commit comments