FAQ and troubleshooting¶
Answers to the questions people ask first, and fixes for the problems they meet.
Setting up¶
"Postgrest plugin is not installed." supabase.offline.from(…) needs supabase-kt's PostgREST
plugin: add install(Postgrest) to createSupabaseClient, before install(Offline). The same
message names Storage when supabase.offline.storage is used without install(Storage).
supabase.offline throws IllegalStateException. The client was built without
install(Offline). Install it last; see Project setup.
"An offline client is already open on … or its data directory." A second client was built over
the same database while the first is open: build the client once per process and share it, or
close() the first before you build another. "Still closing" means the first is releasing the
database: call its close() again to wait for that.
createSupabaseClient throws on Android, naming androidx.startup. The plugin takes the
application context from androidx.startup's InitializationProvider. If your manifest removes
that provider, the plugin cannot open its database; keep the provider, or at least the library's
initializer under it.
A read, an update or a delete throws IllegalArgumentException: "two top-level filters on" a
column. supabase-kt would send only the first: put them in one and { } group; see
Coming from supabase-kt.
The first read offline is empty¶
The cache holds the answers the server gave, one per request: a request's columns, filters, order
and range make its key. A request never answered online has nothing cached, so offline a one-shot
read fails, and a live read shows nothing but the queued writes it admits (null rows when there
are none), even when another cached request of the same table holds the rows it asks for. Read a
detail screen's row from the list a list screen already reads, with the same request, and pick it
out on the device, as the samples do
(One-shot reads). And read each list online once: on
the very first launch, before any read has reached the server, there is nothing to show offline.
The fallback takes seconds¶
With nothing cached, a read that fails at the transport waits out supabase-kt's own retries (four
attempts, about seven seconds) before it throws, since there is nothing to fall back to. With the
query cached, a NetworkFirst read falls back at once, and a read sent while the network is known
to be down goes out once, without the retries. A server that answers 503 is retried on
supabase-kt's schedule and then thrown, cached or not. See
One-shot reads.
A list that never finishes loading¶
QuerySnapshot.result == null means nothing is known yet; an empty body means the server said the
list is empty. A list that stays null with refresh at Refreshing is waiting, for the network
or for a stored session still restoring (which never reads Stalled), or, under
ResponseTrust.AuthenticatedSessionOnly, it is an empty table read signed out
(Refresh lifecycle).
Reading back a queued row¶
A queued write shows in cached reads at once, as it was sent. The rows a queued insert or upsert
adds come after the server's rows, whatever the request's order and limit, so sort on the device
when the order matters before the write is sent. They lack the columns the server fills in
(defaults, triggers) until the server has the write: decode them into a type whose server-filled
fields are optional, or write those columns yourself. A write whose result you read needs
resultOrNull(), which is null while the write is queued; see
Coming from supabase-kt.
A write returned Queued while online¶
A write is queued behind any earlier write of the same user still waiting, in any table, so it
never overtakes it; the library sends it a moment later. It is also queued while a stored session
restores, and when it was sent but may not have run (no answer, a 503). See
Writes and the offline queue.
Writes are not sent when the app comes back¶
Nothing fires when the app comes to the foreground. After a pass that failed in a way that may
pass, the library's own next pass waits up to a minute. Call syncPendingMutations() as the app
comes to the foreground; see Replaying the queue.
Upserts¶
An upsert merges into the row with the same onConflict columns, or the same key: id, unless you
set the table's key with primaryKey(table, columns). On a table without an id column, name the
conflict columns, or a queued upsert shows as a second row until the list is fetched again. An
upsert that ignores duplicates shows as though it landed, since offline the library cannot tell
whether the server holds the row already. See Optimistic overlay.
A change made in the dashboard does not show¶
The library does not poll. A cached list changes when your app writes through the library, when
the list is fetched again, and when you apply a Realtime record. Fetch the list again (a
NetworkOnly read of the same query, for a pull to refresh), or apply Realtime's records; see
Reactive reads.
A debug log line says a request was stopped¶
With supabase-kt's logging at DEBUG, each read logs a failed GET "stopped by the offline
engine's read-shape check". That request was never sent: it is how the library learns a read's
shape without asking for a token.
iOS builds¶
In a Kotlin Multiplatform app, the library is a dependency of your shared module and ships inside
the framework that module builds, like supabase-kt itself; there is nothing more to add in Xcode. It
supports iosArm64 and iosSimulatorArm64, iOS 15 and later. A native Swift app uses the Swift
package instead: iOS apps in Swift.
Using the library alongside Room¶
The library keeps its own Room database, supabase_offline.db, beside any database your app has;
the two never share a file or a connection. It runs on the SQLite the library bundles. Do not open
supabase_offline.db yourself, and do not keep in Room a copy of data the library already caches
from Supabase: the library is the offline cache for those tables.