Local-only mode: images and media fail to load offline on android

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

  1. Put the Android device in airplane mode. Disable Wi-Fi and cellular. Make sure no VPN is running.
  2. Open Anytype (Local-only vault) and create a new page.
  3. Insert an image from the gallery (or share an image into Anytype).
  4. 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

  1. 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).
  2. Switch back to Anytype. Within ~5 seconds the previously-failed image renders instantly, with no network traffic generated.
  3. 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 a tun0 interface.

Scenario C — cross-device sync over hotspot requires restart to display new media

  1. Android device: enable airplane mode, then enable Wi-Fi hotspot. Start RethinkDNS in Firewall mode (so a tun0 interface exists and localdiscovery sees a multicast interface).
  2. 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.
  3. On the desktop, open a shared space and upload a new image into a page.
  4. 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”.
  5. 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):

  1. localdiscovery sees no usable network interface in airplane mode. The Go core enumerates OS network interfaces every 5 seconds (localdiscovery_android.go, periodicCheck). It filters them with filterMulticastInterfaces, keeping only interfaces that are Up + Multicast + non-Loopback + has-an-IPv4. In pure airplane mode there are zero such interfaces, so the discovery state stays DiscoveryNoInterfaces and mDNS never starts. No mDNS → no local peers discovered → no peer-to-peer block fetch possible.

  2. 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 calls fileObjectService.GetImageData, which first has to resolve the object’s spaceId via resolveSpaceIdWithRetry (idresolver/resolver.go). This retry is Attempts(0) = infinite, with a 1-minute outer timeout from getFileTimeout in gateway.go. If the indexer has not yet written the objectId → spaceId binding 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 to proxyStore.Get → origin.Get, which tries getFromLocalPeers (no peers because no mDNS) and getFromNodePeer (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.Getorigin.GetgetFromLocalPeers path described above. The Android client only discovers and refreshes the local peer list on certain events (network-state change, localdiscovery periodic 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 next refreshInterfaces cycle 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

Suggested fix direction

  1. In Local-only mode, the gateway should serve image/file requests directly from localStore without going through resolveSpaceIdWithRetry and the full object-load path — the file ID + file keys are enough to decrypt and serve.
  2. The retry.Attempts(0) (infinite) in gateway.getImage and in ResolveSpaceIdWithRetry should be bounded (e.g., 5 attempts with backoff), so a transiently-missing block fails fast instead of hanging for 60 seconds.
  3. When localdiscovery is in DiscoveryPossible state 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.
  4. 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.

Thanks for reporting this, we’ll take a look at it. However, it’s worth noting some key points here:

You do not need local-only mode for travel or poor-connectivity environments. Local-only does not mean offline-first. Anytype is designed as a local-first product as the default, which supports offline usage.

Local-only is an entirely different proposition.

We have this explicitly stated in the product, that local-only mode is an experimental mode. In other words, it’s not a mode we actively dedicate resources to and support as a fully-fledged feature. As mentioned, Anytype is a local-first product so it’s designed to use e2ee sync as the default. This is why you’re seeing the artifacts of this behaviour in local-only.

We only provide local-only as an experimental feature because it’s something a small portion of users want to use. But many of the issues stem from its incompatibility on an architecture level, which makes it very difficult to support.

If you want to use Anytype while travelling or have poor-connectivity environments, you can simply use the default local-first mode (connect to Anytype Network for sync). We would also argue that this does not pose any privacy threat either because your encryption keys are generated locally and data is synced via e2ee.

First, a practical note: I had to make a new account for this reply because I forgot my password and the reset flow on this forum doesn’t seem to work. So if the username looks unfamiliar, that’s why.

Thanks for taking the time to reply. I appreciate the honesty about where local-only sits in the priority list. I want to push back on a few points, not to be difficult, but because I think the answers deserve a closer look.

On “experimental.” Local-only mode shipped in December 2023 as a headline feature of v0.37.0. The release announcement listed it as a highlight, not a preview. The “experimental” label appeared about four months later, in a forum comment from Razor in April 2024. The in-app warning dialog landed in August 2024, roughly ten months after launch. It is now June 2026. The feature has existed for two and a half years and has carried the “experimental” label for over two. I understand why the label is there. But at some point “experimental” stops meaning “early and unpolished” and starts reading as “we don’t intend to fix this.” The official FAQ still lists local-only as a normal option with no caveat, which makes the in-app warning feel less like a safety notice and more like a liability shield. I don’t think that’s the intent, but it’s how it lands.

On the architecture argument. You’re right that Anytype wasn’t architected local-only from the ground up. I’ve read the code, and that’s clear. The gateway, the file sync pipeline, the localdiscovery layer, the peer store, all of it assumes a network exists. But “the architecture doesn’t support it” is a statement about choices the team made and continues to make, not a law of physics. The file sync queue already short-circuits in local-only mode. The localdiscovery periodic check already runs every five seconds. The pieces are there. What’s missing is the wiring to make the display path not depend on a network interface existing, and the willingness to treat “no network” as a first-class state instead of a degraded one. That’s a resource allocation decision. It’s fair to say you’ve chosen not to allocate there. It’s less fair to say the architecture prevents it.

On “not actively dedicating resources.” This is the part that’s hard to sit with, and I’ll be honest about why. I understand that a small portion of users want local-only. But the same release cadence that deprioritizes local-only is shipping chat, discussions, an AI assistant, an MCP server, publishing, and a builder plan. The community thread asking the team to stop with AI has 246 likes and 94 posts. Find-and-replace was requested in January 2022 and only entered active development last month, four and a half years later. The export bug I filed alongside this one silently drops synced media from backups. None of those are local-only problems. They’re prioritization decisions. When a team says “we can’t dedicate resources to local-only” while dedicating resources to a chat system and an AI agent that a large chunk of the community didn’t ask for, the message isn’t “we lack resources.” The message is “we chose where the resources go, and local-only users aren’t it.” That’s a valid business decision. But it would be more honest to name it as a priority call than as an architecture limitation.

On “just use default mode with E2EE.” I think this misunderstands why some of us choose local-only. Privacy isn’t the only reason. Default mode leaks metadata. The Anytype Network knows your IP, when you’re online, your peer graph, your sync timing, your storage usage. E2EE protects content. It doesn’t protect patterns. For someone in a hostile network environment, or an authoritarian jurisdiction, or simply someone who doesn’t want their note-taking app to require a round-trip to a server to display a thumbnail they uploaded five seconds ago, “just use the network” isn’t an answer. Local-only is about sovereignty, not secrecy. And the bug I reported proves the point: in local-only mode, fully offline, uploading an image and then displaying it fails until a VPN creates a fake network interface. The local blocks are on disk. The app has the keys. There’s no technical reason it can’t render. It fails because the display path was never wired to handle “no network” as a real state. Telling that user to connect to the Anytype Network is, I think, conceding that local-only doesn’t work and asking the user to give up the thing they chose it for.

I’ll be straight about where this leaves me. The VPN workaround I found works, but it doesn’t work on all Android versions, and relying on a third-party firewall app to make a local-only note tool display its own locally-stored images is not a sustainable foundation. My hope is that either the display path and the export path get fixed so local-only actually works without a network, or that the mode is left alone as it is.

What I’m worried about is the middle scenario: that the connectivity check gets “hardened” in a way that closes the VPN workaround without fixing the underlying dependency. If that happens, I’m left choosing between staying on old versions indefinitely, or forking both codebases and maintaining the patches myself. Forking is a real option given the license, but maintaining a fork against an actively-moving upstream is a serious commitment, and it leaves me unable to take security updates without re-applying my changes each time. That’s not a stable place to be. If the trajectory keeps tightening toward cloud-dependent defaults, the honest choice for users like me is either a complete app switch or a self-maintained fork, and both of those come with real costs that I’d rather avoid.

I’m not asking for chat to be dropped or AI to be cancelled. I’m asking that a mode the team chose to ship, chose to keep in the product for two and a half years, and continues to list in the FAQ as a real option, actually function for the one thing it’s supposed to do. If it can’t, the cleaner move might be to remove it, rather than label it experimental indefinitely and point users toward the cloud.

Thanks for your message.

I agree with pretty much most of what you’re saying in essence. We choose not to prioritise it and the reason why we don’t dedicate resources to it is because it requires a lot of effort (architectural differences) for too little reward. We’re not hiding behind ‘arhitecture doesn’t support it’ as if it’s an impossible obstacle—clearly it’s not. We are indirectly saying it’s not worth it for us at this stage, but maybe I should’ve been clearer with that.

Indeed. We are highly considering this but haven’t done so because it seems fine to leave the situation as is (experimental label to lower expectations and use on your own discretion). Local-only is the cause of a lot of support tickets and resources for our team that does not justify its current value.

If you believe we’re misrepresenting expectations of local-only support, I will rectify that (such as adding notes to the docs).

Thanks for the straight answer.

On the docs. Yes, please. If local-only is staying in the product as-is, the docs and the FAQ should reflect what it actually is right now: a mode that works for text and basic editing but has known failures with media display and export when no network interface is present, and that depends on the Anytype Network for reliable cross-device file sync. The in-app warning already says “experimental,” but the docs still present it as a normal, fully-supported option with no caveats. That gap is what made me file this as Critical in the first place. If the docs matched the reality, users like me would go in with eyes open instead of discovering the limitations by hitting them. A note in the docs won’t fix the bug, but it stops the next person from feeling misled.

On the support ticket burden. I believe it. Local-only users are probably overrepresented in support tickets because we’re the ones hitting edge cases the architecture wasn’t built for. I can see how that becomes a vicious cycle: the mode generates tickets, the tickets take resources, the resources aren’t there because the mode isn’t prioritized, so the tickets pile up. I don’t envy the position. But I’d push gently on one thing: the support burden is partly a function of the docs gap. If the docs said “this mode does not reliably display media offline and requires both devices to be connected for export to work,” a lot of those tickets would never get filed. People would either accept the limitations or use default mode. Right now users hit the bug, don’t know it’s a known limitation, and file a ticket thinking something is broken when it’s actually by-design neglect.

On removal. I understand the consideration. If the team is genuinely weighing whether to remove local-only, I’d ask that the decision be communicated before it happens, not after. A forum post explaining the reasoning, a migration path for existing local-only users to default mode without losing data, and a clear timeline would go a long way. Pulling the mode silently or with a changelog line would burn trust that the community has built over years. I don’t want to see it removed. But if it’s going to be, then so be it.

On the bug itself. I’ll be honest about my hope here. I don’t expect local-only to become a first-class priority, and I accept it. What I’m hoping for is narrower: that the specific display path failure, the one where locally-stored blocks won’t render without a network interface, gets looked at even if the broader mode stays deprioritized. The VPN workaround I found works, but not on all Android versions, and it’s fragile. If the gateway could serve from localStore directly in local-only mode, bypassing the resolveSpaceIdWithRetry and peer-fetch paths that assume a network exists, that alone would fix Scenario A without requiring a full architecture overhaul. It’s a targeted fix, not a rebuild. Whether it’s worth the engineering time is the team’s call. I’m just naming the specific ask so it’s on the record.

Same for the export bug. If the exporter could use the same wait-for-available behavior the gateway uses, or at minimum surface a warning when files are dropped instead of reporting success, that’s a small change with real impact for anyone using export as a backup path. Both bugs are in the same code area. If someone’s already in there for any reason, the fixes are nearby.

I’ll leave it there. I know local-only users are a minority and I’m not asking for the roadmap to revolve around us. I’m asking that the mode either work for its core use case or be documented accurately enough that people know what they’re getting into. Either of those is fine. The current middle ground, where it’s marketed as a real option but supported as an afterthought, is the part that hurts.

I am a local-only user. I have faced the problems you mentioned and I fully support your suggestions.

Again, thanks for taking the time to write out your suggestions and thoughts.

In short, I will update the docs to reflect that this is not a feature that’s fully supported. Along with the experimental flag in the product, this should hopefully clearly communicate to users that they will likely get a sub-par/de-prioritised experience.

I’ve filed your bugs already, they may or may not get fixed depending on priorities. Sometimes we’re fixing one thing that’s related to another, which then leads priorities changing—which happens in both directions.

If we remove the feature (which we have no plans on doing right now), then we’d communicate this. As mentioned this ‘middle-ground’ option is where we’ve landed because it’s simply the least amount of effort that’s already higher than what we probably should be spending. Everything you’ve suggested is effort nonetheless, and this is landing on a team that many already consider to be working too slow to fix other bugs/features that affect a much larger portion of the community.

Last point: it’s not that we don’t think local-only has value—it absolutely does. It just doesn’t work well in the current setup (architecture, user base, business model, etc.) which makes the resources-to-reward ratio not add up. We may share more on this in the future as it’s certainly something that’s aligned with sovereignty—even if it’s not a popular setup among most users.

I jumped at Anytype because of the local-only mode, that as mentioned in this conversation was not announced as an experimental or second-class option at all. Since them, each thread in this forum about local-only mode makes clear you AnyType wants to get rid of it but doesn’t know how to do it without some drama.

Honestly, get rid of it. We local-only users are going to leave sooner or later. We wait too much, and we will hit some problems you won’t fix (i.e. I can’t recover some zip files, they stay in downloading forever) and our impression of the product and the company will get worse and worse. So better rip it off in one strike like a band-aid.