Summary
Pairing from the native Android APK (Capacitor WebView) consistently fails with:
helm/transport-supabase: subscribe failed with status CHANNEL_ERROR: channel error: transport failure (channel private:helm:...)
The exact same pairing flow works fine in a mobile browser (Chrome) on the same phone/network, so this is specific to the Capacitor WebView runtime, not a network-wide or account-wide issue.
Environment
- Device: Samsung SM-S906W (Galaxy S22+ variant), Android 16
- WebView:
wv tag, Chrome/149.0.7827.164 (confirmed current, not stale)
- App: helm-debug.apk (debug-signed, package
dev.aasis21.helm, minSdk 23, targetSdk 35)
- Transport:
shared/transport-supabase.mjs (Supabase Realtime channels)
Ruled out so far
- Supabase project health:
ACTIVE_HEALTHY
- RLS policies on
realtime.messages (helm_receive_broadcast / helm_send_broadcast, scoped to private:helm:%) — correct
- AndroidManifest permissions —
INTERNET present; no CSP in index.html
VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY — verified correctly embedded in the built APK's JS bundle
- WebView staleness — ruled out, UA shows Chrome/149 (very current)
- Code branching —
createTransport / helmClient.ts / transport-supabase.mjs have no native-vs-web branch anywhere in the connect/subscribe path (only QR-scan UI and getSenderName() branch on Capacitor.isNativePlatform(), neither touches this flow)
Root cause of the generic message
@supabase/realtime-js's normalizeChannelError falls back to the generic "channel error: transport failure" string whenever the underlying failure reason has no numeric .code (i.e. it's a plain WebSocket error Event rather than a CloseEvent). Browsers/WebViews never enrich a raw socket error event with a reason, by design — so this message alone can't tell us why the connection failed.
Diagnostics shipped
-
mobile/src/lib/debugSettings.ts — persisted Debug mode toggle + describeError() (platform/transport/UA/online-status/error-cause chain)
-
mobile/src/components/JoinSessionScreen.tsx — debug toggle + expandable technical-details panel on pairing failure
-
mobile/src/lib/wsProbe.ts + "Run connectivity test" button (visible when Debug mode is on) — opens a raw WebSocket (bypassing supabase-js) to:
- a public echo server (
wss://echo.websocket.org) — proves the WebView can open any external WebSocket
- the actual configured Supabase Realtime endpoint — proves reachability of that specific host
Reports the real CloseEvent.code / reason, or a timeout/error, instead of supabase-js's swallowed generic message.
Live APK: https://usehelm.netlify.app/app
Next steps
- Install the latest debug APK, enable Debug mode, retry pairing, run "Run connectivity test", and capture the output (both probe results with any close codes).
- Interpret results:
- Echo probe fails too → WebView-level or network-level block on all external WebSockets from the app (e.g. a VPN/firewall/DPI rule keyed on user-agent or app identity)
- Echo probe succeeds but Supabase probe fails/times out → issue is specific to the Supabase Realtime host (carrier/firewall block, DNS, or an upstream Realtime/Capacitor compatibility issue)
- Supabase probe returns a numeric close code → look up Phoenix/Realtime close-code meaning to pinpoint the failure stage (auth, upgrade, TLS, etc.)
- Based on findings, likely fixes: adjust network/DNS, work around carrier/firewall WebSocket blocking, or file an upstream issue against
@supabase/realtime-js / Capacitor WebView interaction.
Related commits
04cda31 — debug mode + technical error details + APK packaging fix
2cc1f9c — raw WebSocket connectivity probe wired into debug panel
Summary
Pairing from the native Android APK (Capacitor WebView) consistently fails with:
The exact same pairing flow works fine in a mobile browser (Chrome) on the same phone/network, so this is specific to the Capacitor WebView runtime, not a network-wide or account-wide issue.
Environment
wvtag, Chrome/149.0.7827.164 (confirmed current, not stale)dev.aasis21.helm, minSdk 23, targetSdk 35)shared/transport-supabase.mjs(Supabase Realtime channels)Ruled out so far
ACTIVE_HEALTHYrealtime.messages(helm_receive_broadcast/helm_send_broadcast, scoped toprivate:helm:%) — correctINTERNETpresent; no CSP inindex.htmlVITE_SUPABASE_URL/VITE_SUPABASE_ANON_KEY— verified correctly embedded in the built APK's JS bundlecreateTransport/helmClient.ts/transport-supabase.mjshave no native-vs-web branch anywhere in the connect/subscribe path (only QR-scan UI andgetSenderName()branch onCapacitor.isNativePlatform(), neither touches this flow)Root cause of the generic message
@supabase/realtime-js'snormalizeChannelErrorfalls back to the generic"channel error: transport failure"string whenever the underlying failure reason has no numeric.code(i.e. it's a plain WebSocketerrorEvent rather than aCloseEvent). Browsers/WebViews never enrich a raw socketerrorevent with a reason, by design — so this message alone can't tell us why the connection failed.Diagnostics shipped
mobile/src/lib/debugSettings.ts— persisted Debug mode toggle +describeError()(platform/transport/UA/online-status/error-cause chain)mobile/src/components/JoinSessionScreen.tsx— debug toggle + expandable technical-details panel on pairing failuremobile/src/lib/wsProbe.ts+ "Run connectivity test" button (visible when Debug mode is on) — opens a rawWebSocket(bypassing supabase-js) to:wss://echo.websocket.org) — proves the WebView can open any external WebSocketReports the real
CloseEvent.code/reason, or a timeout/error, instead of supabase-js's swallowed generic message.Live APK: https://usehelm.netlify.app/app
Next steps
@supabase/realtime-js/ Capacitor WebView interaction.Related commits
04cda31— debug mode + technical error details + APK packaging fix2cc1f9c— raw WebSocket connectivity probe wired into debug panel