Severity: Critical — This bug defeats the core promise of Local-only mode (fully offline, self-contained operation). Users who rely on Local-only mode for privacy, travel, or poor-connectivity environments lose access to their own locally-stored media whenever the device has no “online” network. The cross-device variant additionally causes silent data-visibility gaps: sync status reports “Connected” while newly-uploaded media remains invisible until a manual app restart, which can easily be mistaken for data loss.
WHAT IS THE BUG
In Local-only mode, images and other media uploaded on the device — or synced from a peer on the same local network — fail to display when the device has no “online” network (no Wi-Fi association, no cellular). The app behaves as if it requires a server round-trip even though Local-only mode is supposed to be fully offline and self-contained. DNS captures confirm nothing is being uploaded anywhere, yet the UI refuses to render thumbnails and image blocks until the device briefly obtains a network association or a TUN interface is created (e.g. by starting a VPN in airplane mode). The local blocks are written correctly and persist on disk — only the display path is broken.
Additionally, when two devices (Android + Linux desktop on Flathub/Flatpak) are connected over the Android device’s hotspot in airplane mode with a RethinkDNS firewall VPN enabled, both Anytype clients report “Connected”, but media uploaded on the desktop does not appear on the Android side until the Android Anytype app is relaunched. The reverse direction (Android → desktop) generally works without restart.
HOW TO REPRODUCE IT
Scenario A — single device, fully offline, freshly uploaded media
- Put the Android device in airplane mode. Disable Wi-Fi and cellular. Make sure no VPN is running.
- Open Anytype (Local-only vault) and create a new page.
- Insert an image from the gallery (or share an image into Anytype).
- The upload appears to succeed — the file is written to local storage — but the image block stays blank / shows a loading spinner / shows “Error while loading picture”. The thumbnail never renders.
Scenario B — workaround that proves the bug is in the display path, not the upload path
- With the device still in airplane mode from Scenario A, open RethinkDNS and start it in Firewall mode (no actual upstream needed — just the local TUN interface).
- Switch back to Anytype. Within ~5 seconds the previously-failed image renders instantly, with no network traffic generated.
- The same effect is reproducible with ProtonVPN (tapping Connect, leaving it in “Waiting for network”), WireGuard, NetGuard, PCAPdroid, AdGuard — anything that calls
VpnService.Builder.establish()and creates atun0interface.
Scenario C — cross-device sync over hotspot requires restart to display new media
- Android device: enable airplane mode, then enable Wi-Fi hotspot. Start RethinkDNS in Firewall mode (so a
tun0interface exists andlocaldiscoverysees a multicast interface). - Connect the Arch Linux laptop to the Android hotspot. Open the Flathub/Flatpak Anytype desktop client on the laptop. Both Anytype clients show “Connected” in sync status.
- On the desktop, open a shared space and upload a new image into a page.
- On Android, open the same page. The image does not appear — the block shows as empty or with a placeholder — even though the Android client still reports “Connected”.
- Relaunch the Android Anytype app. The image now appears immediately.
THE EXPECTED BEHAVIOR
- Local-only mode should never require any network interface for media to display. Blocks are written locally; the gateway that serves the local UI should read directly from the local block store. The presence or absence of Wi-Fi/cellular/VPN should be irrelevant.
- The “Use cellular data” toggle under Settings → Files Storage should have a visible effect on offline behavior. Currently it appears to do nothing for the on-demand image-load path.
- A Wi-Fi hotspot created on the Android device itself should be treated as a usable local network for P2P sync between the host and tethered clients — especially since both clients explicitly report “Connected”. New media uploaded on either side should appear on the other side without requiring an app restart.
- The Android client should not require a force-stop + relaunch to pick up media that was just synced from a peer it is actively connected to. The block availability change should be propagated to the gateway / image-load path live, the same way text changes propagate.
ADDITIONAL CONTEXT
Root cause (summary of investigation)
The connectivity-state value (WIFI / CELLULAR / NOT_CONNECTED) reported by Android’s ConnectivityManager is not what actually blocks the media. The real blockers are two separate mechanisms, both in the Go core (anytype-heart):
-
localdiscoverysees no usable network interface in airplane mode. The Go core enumerates OS network interfaces every 5 seconds (localdiscovery_android.go,periodicCheck). It filters them withfilterMulticastInterfaces, keeping only interfaces that areUp + Multicast + non-Loopback + has-an-IPv4. In pure airplane mode there are zero such interfaces, so the discovery state staysDiscoveryNoInterfacesand mDNS never starts. No mDNS → no local peers discovered → no peer-to-peer block fetch possible. -
The gateway’s image-load path has an infinite-retry loop. When the UI requests
http://127.0.0.1:<port>/image/<id>, the gateway callsfileObjectService.GetImageData, which first has to resolve the object’sspaceIdviaresolveSpaceIdWithRetry(idresolver/resolver.go). This retry isAttempts(0)= infinite, with a 1-minute outer timeout fromgetFileTimeoutingateway.go. If the indexer has not yet written theobjectId → spaceIdbinding into the local store (a race that is especially likely right after upload), the gateway hangs for up to 60 seconds and then returns HTTP 500. For images synced from another device whose blocks are not yet in the local store, the load falls through toproxyStore.Get → origin.Get, which triesgetFromLocalPeers(no peers because no mDNS) andgetFromNodePeer(no file nodes in Local-only mode — config is empty), so both paths fail and the infinite retry kicks in.
Why the VPN trick works (without using any internet)
Starting any VpnService-based app in airplane mode causes Android to create a tun0 interface. tun0 reports FlagUp, FlagMulticast, no FlagLoopback, and has an IP — so it passes filterMulticastInterfaces. localdiscovery transitions from DiscoveryNoInterfaces to DiscoveryPossible, fires its state-change hooks, and the rest of the pipeline re-evaluates pending operations. The gateway’s stuck retries then complete (often the indexer is nudged, or the proxyStore short-circuits to the local block hit now that local state is consistent). No actual traffic leaves the device — confirmed by RethinkDNS’s own firewall logs showing zero outbound packets during the fix.
This is also why a Wi-Fi hotspot created on the Android device does not work as a workaround: the SoftAP interface is owned by the Tethering subsystem and is not enumerated as a usable multicast interface for the host. The host’s NetworkInterface.getNetworkInterfaces() does not gain a usable client interface just because it is acting as an AP.
Why the desktop→Android direction requires an Android restart
Both clients report “Connected” because the underlying sync session over the hotspot is alive — tree changes (text edits, new object creation) propagate fine. The problem is specifically the file-block pipeline:
- The desktop uploads the image and writes its blocks to its own local store, then queues them for sync.
- The Android side receives the tree change (the new file object with its file ID and metadata), but the actual file blocks (the encrypted bytes) need to be fetched from the desktop peer via the file-block RPC.
- This fetch is gated by the same
proxyStore.Get→origin.Get→getFromLocalPeerspath described above. The Android client only discovers and refreshes the local peer list on certain events (network-state change,localdiscoveryperiodic check). When the desktop has just appeared on the network and the Android client is already running, the peer list may be stale until the nextrefreshInterfacescycle fires, or until a state-change hook nudges the peer store. - Even after the blocks are fetched and written to the Android local store, the gateway’s image handler may already have an in-flight infinite-retry request that was started before the block arrived. That request does not get cancelled and re-issued against the now-populated local store — it keeps retrying against the stale “block not found” state until its 60-second timeout. The image therefore appears stuck until the app is restarted, which clears the in-flight requests and lets the gateway serve from the freshly-cached blocks.
In short: the sync layer is doing its job (tree changes propagate, blocks get fetched eventually), but the display layer is not wired to react to “blocks just became available locally”. It either has a stuck retry loop or doesn’t get a notification to retry.
Workaround
Until this is fixed, the most reliable workaround on Android is to keep a VpnService-based app running in firewall/local-only mode (RethinkDNS Firewall mode is ideal — it does not require an upstream and adds no traffic). This keeps a tun0 interface alive, which keeps localdiscovery in DiscoveryPossible state, which keeps the display path functional. For the cross-device case over a hotspot, the Android app must still be force-stopped and relaunched after new media is uploaded on the peer.
Related reports
- Images are not showing up when uploaded in offline mode
- Image sync failure in local mode
- P2P Sync between two Android devices does not work on mobile hotspot
- Local-only Vault suddenly syncing to Anytype network after update to 0.54.0
- If sync is set to "local only" images added to page on desktop not synced to android
- In local-only mode, Anytype should open faster. · Issue #750 · anyproto/anytype-heart · GitHub (“In local-only mode, Anytype should open faster”)
- Anytype taking too long to open without internet connection · Issue #1223 · anyproto/anytype-heart · GitHub (“Anytype taking too long to open without internet connection”)
- Unable to local sync android to desktop app · Issue #755 · anyproto/anytype-kotlin · GitHub (“Unable to local sync android to desktop app”)
Suggested fix direction
- In Local-only mode, the gateway should serve image/file requests directly from
localStorewithout going throughresolveSpaceIdWithRetryand the full object-load path — the file ID + file keys are enough to decrypt and serve. - The
retry.Attempts(0)(infinite) ingateway.getImageand inResolveSpaceIdWithRetryshould be bounded (e.g., 5 attempts with backoff), so a transiently-missing block fails fast instead of hanging for 60 seconds. - When
localdiscoveryis inDiscoveryPossiblestate and a previously-missing block becomes available locally (via sync), the gateway should be notified to cancel any in-flight stuck retry and re-issue the request against the now-populated local store. - The “Use cellular data” toggle under Settings → Files Storage should either be removed (if it truly only controls the background auto-downloader) or its scope should be expanded to also affect the on-demand load path, so users aren’t misled into thinking it helps with offline display.
TECHNICAL INFORMATION
Android:
Android version: 33
App version: 0.47.3
Build number: 4703
Library version: 0.50.8
Desktop:
Device: Arch Linux (Flathub/Flatpak build)
App version: 0.55.5
Network setup for Scenario C:
- Android hotspot in airplane mode (no cellular, no upstream Wi-Fi)
- RethinkDNS running in Firewall mode on Android (creates
tun0, blocks all outbound traffic) - Arch Linux laptop connected to the Android hotspot as a Wi-Fi client
- Both Anytype clients in the same Local-only vault, P2P sync over the hotspot
CODE-LEVEL ANALYSIS (may not be perfectly accurate)
The root cause is a combination of (a) localdiscovery seeing no multicast interfaces in airplane mode, which keeps the space/P2P status in a “NotPossible” state that may block or delay space loading and file operations, and (b) the gateway’s DoFileWaitLoad path depending on space loading completing, which may hang on network probes that never complete without a connected network. VPN’s tun0 interface triggers localdiscovery’s state transition from DiscoveryNoInterfaces to DiscoveryPossible, which fires hooks that ripple through p2pStatus and potentially unblock stuck space-loading operations, allowing DoFileWaitLoad to complete and the gateway to serve images from localStore.