Skip to content

On-device data

Everything the library keeps on the device is in one SQLite database, supabase_offline.db, and one directory of downloaded files, both in the app's private storage. This page lists where they are, what they hold, and what removes it.

Where it lives

Database Downloaded files
Android, by default The app's database directory: getDatabasePath("supabase_offline.db") <filesDir>/supabase_offline/
iOS, by default Library/Application Support/supabase_offline.db Library/Application Support/supabase_offline/
With dataDirectory set <dataDirectory>/supabase_offline.db <dataDirectory>/

On iOS, a database found in Documents, where an earlier build kept it, is moved to the default place with its side files and its file cache when the client opens, unless the default place already holds a database (then it is left, and a warning logged). Documents is for the user's own files: with file sharing on, the Files app shows them, and the user can delete them.

Beside the database, SQLite keeps its write-ahead log and its index, supabase_offline.db-wal and supabase_offline.db-shm. On iOS the plugin excludes its files from iCloud and device backups; on Android you leave them out of Auto Backup yourself (Project setup).

The database runs on the SQLite the library bundles, so every install, on both platforms, runs the same SQLite version.

The files

A downloaded object is stored at storage/<bucket>/<object path> under the files directory, when its bucket and path are short printable names. Any other name, and every image-transform variant, is stored under storage-hashed/, in a file named by a hash of its key. A download is written to a temporary file first and moved into place whole; a temporary file a crash left behind is deleted once it is more than a day old.

The database

Room schema version 1, identity hash d82a2435c50b4595368573f8728a370b.

object_cache

One cached list per row, and the library's own records in reserved rows. Primary key: table_name, signature.

Column Type Null What it holds
table_name TEXT no The table, as schema.table outside the default schema; or the reserved name of the library's records
signature TEXT no The cache key: the table, its sorted query parameters and its range
payload TEXT no The JSON array PostgREST answered, as text; for a reserved row, the record
updated_at INTEGER no When the server sent the list, in milliseconds since 1970; negated while the list is stale
synced_through INTEGER, default 0 no The newest replayed write the list has seen, so the overlay skips it

storage_objects

One downloaded Storage object per row; the file itself is on disk. Primary key: key. It is unique on (bucketId, path).

Column Type Null What it holds
key TEXT no <bucket>::<path>
bucketId TEXT no The bucket
path TEXT no The object's path in the bucket
localPath TEXT no Where the file was written; its place is always derived from the bucket and path
sizeBytes INTEGER yes The file's size
eTag TEXT yes Not used: always NULL
lastFetchedEpochMs INTEGER no When the file was downloaded, in milliseconds since 1970: CachedFile.fetchedAt

mutation_cache

The mutation queue: one write per row, in id order. Primary key: id, autoincrement. It is indexed on (owner, synced_at_ms) and indexed on (table_name).

Column Type Null What it holds
id INTEGER no The write's id and its place in the queue: PendingMutation.id
table_name TEXT no The table the write targets
operation TEXT no INSERT, UPSERT, UPDATE or DELETE
body_json TEXT no The body the write sends, as JSON text; {} for a DELETE
filter_json TEXT yes The UPDATE's or DELETE's filter, encoded so a replay rebuilds it
created_at_ms INTEGER no When the write was made, by the device's clock
synced_at_ms INTEGER yes When a pass replayed it; NULL while it waits
default_to_null INTEGER yes The insert or upsert's defaultToNull
on_conflict TEXT yes The upsert's onConflict columns
ignore_duplicates INTEGER yes The upsert's ignoreDuplicates
owner TEXT yes Who made the write: user: and the user's id, or anonymous when nobody was signed in; NULL when none was recorded
sync_sequence INTEGER yes The order in which replayed writes were accepted, against the lists that have seen them
schema_cache_misses INTEGER, default 0 no How many times PostgREST's schema cache did not know the table or a column yet
send_unconfirmed INTEGER, default 0 no Whether a send may have reached the server without its answer arriving

The library's own records (the rejected list, whose cache it is, and each cached list's query shape) are JSON text in reserved rows of object_cache, under a table name 94 bytes long: Postgres cuts identifiers at 63 bytes, so no table of yours can have that name, and from() refuses it. A reserved row is never evicted, counted against cacheSizeBytes, read as a query or overlaid.

What removes it

Event What goes What stays
clearLocalData() Every cached list, every downloaded file, every queued write (never sent) and the rejected list. The database is rewritten without them, as far as open reads allow (details) The database file itself, empty
A user signs out, or another signs in (with Auth) Once the session settles as someone else: the cached lists and the downloaded files The mutation queue and the rejected list: each user's writes wait for that user
Sign-out without Auth, or with your own token provider Nothing: there is one identity for the life of the install Everything
close() Nothing Everything, for the next client
Eviction Cached lists fetched longest ago, past cacheSizeBytes; files used least recently, past fileCacheSizeBytes The queue, which is never evicted; the rejected list keeps its newest 100 entries (see Conflicts and failures)
Uninstalling the app Everything: it is all in the app's private storage

Schema versions

0.1.0 ships schema version 1. A later schema change is a new version with a migration that keeps the queued writes, and a minor release of the library. Going back to an older version of the library across a schema version is not supported, and loses the writes queued since. See Compatibility.