Skip to content

Signing out and switching users

With supabase-kt's Auth plugin installed, the query cache and the file cache belong to one user at a time: when the session settles as someone else they are dropped, and nothing cached for one user is ever served to the next. Queued writes stay with the user who made them until you call clearLocalData(), which deletes everything the library keeps on the device:

// Send what can still be sent as this user; offline this returns at once
supabase.offline.syncPendingMutations()
try {
    supabase.auth.signOut()
} finally {
    // Offline, signOut() throws and supabase-kt keeps the session: clear the device anyway.
    // Every write still queued is deleted here, never sent
    supabase.offline.clearLocalData()
}

Signing out in supabase-kt alone clears the session, not the device.

What clearLocalData() deletes

The cached query results, the downloaded Storage files, every write still in the mutation queue (never sent) and the rejected list. It does not touch the server, and the client stays usable.

  • A replay pass running at that moment is canceled first, and nothing queued is sent once the clear has begun.
  • A write already on the wire is neither applied to the cache nor queued when it answers. A Storage download in flight may still hand its bytes to its caller, but they are not cached.
  • A live read open through the clear that refreshes (any strategy but CacheOnly) fetches again: at once online, and once the network is back offline (Paused meanwhile).
  • Keeping the deleted rows out of the database's files is best effort. The database is rewritten without them, and its write-ahead log, which still holds them, is emptied once no open read holds it: the clear waits up to a second for such reads, and when one outlasts that, the library empties the log within a few seconds of the read ending, and once more at close(). It never fails the clear for it.

Order of operations

  1. Decide about the unsent writes. syncPendingMutations() sends what can be sent now, as the user who made it; offline it returns at once. The clear deletes whatever is still queued, so an app that must not lose writes can show syncStatus.value.pendingMutationCount and ask first.
  2. Sign out, and clear in a finally. Offline, supabase-kt's signOut() throws HttpRequestException and keeps the session, since it clears the session only when the server answers; clearing in a finally deletes the device's data either way. The listing above does the first two.
  3. Decide about the session. After an offline sign-out the user is still signed in: the exception reaches your code with the session kept. To end it on this device anyway, forget it without the server; the server-side session stays valid until it expires:

    try {
        supabase.auth.signOut()
    } catch (offline: HttpRequestException) {
        supabase.auth.clearSession() // forgets the session here, without asking the server
    }
    

Switching accounts

Facts about the engine, with the Auth plugin installed. The query cache and the Storage file cache belong to one identity at a time: a signed-in user, or nobody signed in.

  • A read never returns what was cached for another identity, and a response or download that started before the switch is never written into the new identity's cache.
  • When the session settles into a different identity, both caches are dropped. The mutation queue is kept whole: each identity's writes wait for it.
  • Without the Auth plugin, or with the app's own token provider, there is one identity, nobody signed in, for the life of the install: the cache is served as it is and never dropped for a change of user, since nothing the engine can read tells one user from another. An app whose own token provider serves more than one user must call clearLocalData() itself when its user changes, and only once the provider has stopped returning the outgoing user's token: a live read open through the clear that refreshes fetches again with whatever token the provider returns then.

Queued writes follow their owner. A write is recorded with the user who made it; another user's writes are never listed, counted, sent, discarded or retried, and their ids do nothing. With Auth installed, a queued write is sent with its owner's token or not at all, so user A's writes wait, untouched, while user B is signed in, and go when A signs back in.

While a stored session restores

Until supabase-kt's Auth plugin has restored the stored session, every request goes out on the anon key, and the library keeps its own requests off it as far as it can. A queued write waits, as the stored user's, and is sent with that user's token once the session settles, online or not. A read the cache can answer for the stored user sends nothing, and one that needs the server is not cached (One-shot reads). An OnlineOnly write, every Storage request and every rpc are sent at once, on the anon key; a download's answer is cached, and a private object refused on the anon key reads as "not found", which keeps its cached copy and is answered from it. Under row-level security the anon key reaches less than the user does, so such a write, rpc or Storage call can be refused, and a private download with nothing cached fails: make them after supabase.auth.awaitInitialization().

An expired session offline is still its user's: a write made with it is queued as that user's and sent once the token is refreshed.

Deleting an account

Delete a user's data on the server, then on the device, online, and clear the device first, so no queued write can be replayed after your deletes and leave a row behind. Send the deletes OnlineOnly: a queued delete could outlive the user it is for. Delete the user's Storage objects through the bucket before anything that deletes the user: Supabase refuses to delete a user who still owns objects, and deleting a row never deletes its file. The sample's Start over does all of it for its anonymous user, whom nobody could sign back in as, then signs in as a new one:

From the samples: OfflineTaskRepository.kt ยท TaskRepository.swift

/** Start over: clear first, so no queued write lands after the deletes; then a new user. */
override suspend fun startOver() {
    // A new anonymous user needs the server, so nothing is deleted or cleared without it.
    if (!canReachNetwork()) throw NetworkUnavailableException()
    detached {
        uploader.pausedWhile {
            val userId = session.userId.value
            offline.clearLocalData() // stops a replay already running, and drops the queue
            userId?.let { deleteEverythingOf(it) }
            // A pending photo's file may be in the bucket already, its row not written yet.
            deleteFiles(pendingPhotos.photos.first().map { it.objectPath })
            pendingPhotos.removeAll() // outside what clearLocalData() deletes
            session.signInAsNewUser()
        }
    }
}


/** Deletes [userId]'s files, photo rows and tasks on the server, before that user is left. */
private suspend fun deleteEverythingOf(userId: String) {
    deleteFiles(photoPathsOnServer(USER_ID, userId))
    // Sent now or not at all: queued, they would outlive the user they are for.
    write(Tables.TASK_PHOTOS, TaskMutationOperation.Delete, taskTitle = null) {
        offline.from(Tables.TASK_PHOTOS).delete(
            mutationStrategy = MutationStrategy.OnlineOnly
        ) {
            filter { eq(USER_ID, userId) }
        }
    }
    write(Tables.TASKS, TaskMutationOperation.Delete, taskTitle = null) {
        offline.from(Tables.TASKS).delete(mutationStrategy = MutationStrategy.OnlineOnly) {
            filter { eq(USER_ID, userId) }
        }
    }
}
/// Starts again as a new anonymous user with no tasks, in this order:
///
/// 1. Photo uploads are held, the one in progress waited for, and this user's queued writes
///    are discarded (a write already being sent is waited for), so none lands after the deletes.
/// 2. Their photo files and rows are deleted from the server, with their token: an anonymous
///    user cannot sign back in to reach them.
/// 3. The client's network is turned off, until the new user is signed in.
/// 4. The app signs out, so the client's token closure returns `nil`.
/// 5. Local data is cleared: the library's, then the photos waiting to upload.
/// 6. The app signs in as a new user, and the network is turned back on: open live reads then
///    fetch again, with the new user's token.
///
/// - Throws: ``SampleFailure/needsNetwork`` while offline, Simulate offline included, when
///   nothing is discarded, deleted or cleared.
func startOver() async throws {
    guard canReachNetwork else { throw SampleFailure.needsNetwork }
    await pendingPhotos.holdUploads()
    defer { pendingPhotos.releaseUploads() }
    for mutation in try await client.pendingMutations().first() {
        try await client.discardPendingMutation(id: mutation.id)
    }
    if let userID = session.userID { try await deleteEverything(of: userID) }
    try await client.disableNetwork()
    await session.signOut()
    try await client.clearLocalData()
    try pendingPhotos.removeAll()
    do {
        try await session.signIn()
    } catch {
        try await client.enableNetwork()
        throw error
    }
    try await client.enableNetwork()
}

/// Deletes `userID`'s files, photo rows and tasks on the server, before that user is left.
/// The deletes are sent now or not at all: queued, they would outlive the user they are for.
private func deleteEverything(of userID: String) async throws {
    try await deleteFiles(photoPathsOnServer { $0.eq("user_id", value: userID) })
    // The files of its photos waiting to upload too: an upload whose answer was lost may have stored one.
    try await deleteFiles(pendingPhotos.photos.filter { $0.userID == userID }.map(\.objectPath))
    try await write(taskTitle: nil) {
        try await client.from(Tables.taskPhotos).delete(mutationStrategy: .onlineOnly).eq("user_id", value: userID).execute()
    }
    try await write(taskTitle: nil) {
        try await client.from(Tables.tasks).delete(mutationStrategy: .onlineOnly).eq("user_id", value: userID).execute()
    }
}

The iOS sample discards the queued writes first and clears after the sign-out, with the client's network off in between: the order the Swift guide's When the user changes gives, and why.

Caveats

On Android, Auto Backup includes the database and the file cache unless you leave them out, and a restore brings them back under whoever signs in next (Backups). The library cannot see a client in another process of your app (A second process).