Skip to content

How it works

The library keeps three things on the device: the answers to your list reads, the writes it has not sent yet, and the files you downloaded. A read is answered by the server or from those answers, with the unsent writes laid over it; a write is saved first and sent when nothing waits ahead of it.

The three local stores

Store What it holds Bound
The query cache The rows the server answered to each list read, one entry per query cacheSizeBytes, 100 MiB by default; the entries fetched longest ago go first
The mutation queue Every write not yet accepted by the server, with the user who made it, in the order it was made; and the writes the server refused, on the rejected list The queue: none, a queued write is never evicted. The rejected list: its newest 100 entries; older ones go, and their writes are lost
The file cache Storage objects you downloaded fileCacheSizeBytes, 256 MiB by default; the least recently used files go first

The first two share one SQLite database, supabase_offline.db, and the files sit beside it (On-device data).

What counts as offline

A failure is the network's when it is a timeout or an I/O failure, such as supabase-kt's HttpRequestException, here or in a cause; an answer from the server, with any HTTP status, never is. Throwable.isNetworkOrConnectivityFailure() is the test, and the library uses it itself: a read falls back to the cache on a connectivity failure only, so the cache never hides a refused request, and a write stays queued when it may not have run (no connection, no answer, a 503 or 520, an expired token that could not be refreshed). Any other answer is the server's decision, and is thrown. The NetworkDisabledException of disableNetwork() counts as a connectivity failure too. The platform's connectivity monitor is a hint, never a gate: every request you make is tried (Replaying the queue).

What is cached

A list of rows, under its cache key: the table, its query parameters sorted by name, and its range (todos?done=eq.false&order=created_at.desc|range=0-49). Another filter, order, column list, page or schema is another entry, fetched and kept on its own, so a query never read online has nothing offline, even when another cached list holds its rows: read offline with the query you read online, and filter in memory. Only lists are cached: single(), a count, head, csv(), geojson(), stripNulls() and explain() go to the server every time, so read one row offline from the list, by its key.

The overlay

A read returns the cached answer with the user's queued writes laid over it, oldest first, so the app shows its own writes at once, online or not; a write sent at once is patched into the cached lists instead (Optimistic overlay).

flowchart LR
    R["select / selectAsFlow"] --> S{"Strategy"}
    S -- "network" --> P["PostgREST"]
    P -- "a list" --> C[("Query cache")]
    S -- "cache" --> C
    C --> O["Overlay of queued writes"]
    P -. "unreachable: fall back" .-> C
    O --> A["Rows to the app"]
    W["insert / upsert / update / delete"] --> Q[("Mutation queue")]
    Q -- "nothing ahead of it" --> P
    Q -- "later, in order" --> P
    Q --> O

Whose data it is

With supabase-kt's Auth plugin, the query cache and the file cache belong to one user at a time, dropped when the session settles as someone else; the queue keeps each user's writes apart and sends them only with that user's token. Without Auth there is one identity for the life of the install (Signing out and switching users).

Glossary

Term Meaning
Cache key The fingerprint a cached list is stored under: the table, its sorted query parameters and its range
Mutation A write: an insert, upsert, update or delete
Pending, queued A write in the mutation queue the server has not accepted yet
Pass One run that sends the user's queued writes, in order, until the queue is empty or something stops it
Replayed A queued write a pass sent and the server accepted. It stays in the overlay until each cached list has been fetched again after it
Rejected A queued write the server refused for good on replay, kept on the rejected list for the app
Refresh A reactive read asking the server again for the rows it shows
Settled session A session that is signed in or signed out, not restoring and not failing to refresh
Stale A cached list a write may have changed in a way the library could not follow; it is fetched again when it can be