How Thunderbolt Handles Account Deletion and Device Revocation Requests

Thunderbolt implements a two-stage reset flow where backend endpoints return specific HTTP status codes (410 Gone for deleted accounts, 403 Forbidden for revoked devices) that trigger a frontend listener to clear local data and force a full page reload, ensuring every client reaches a clean signed-out state.

The Thunderbird Thunderbolt project manages sensitive user state transitions through a coordinated architecture spanning REST endpoints, PowerSync token services, and React hooks. Understanding how Thunderbolt handles account deletion and device revocation requests requires examining the specific HTTP status codes, custom browser events, and database cleanup routines that ensure consistency across distributed clients.

Backend Endpoints and Token Service Logic

Account Deletion and Device Revocation Endpoints

The backend exposes dedicated endpoints for permanent account removal and device management. In backend/src/api/account.ts, the implementation handles:

  • DELETE /v1/account: Permanently removes the user and all related data via hard delete, returning 200 on success.
  • POST /v1/account/devices/:id/revoke: Marks a specific device as revoked by setting a revoked_at timestamp, returning 204 on success.

PowerSync Token Endpoint Validation

The critical validation logic resides in the GET /powersync/token endpoint. This handler checks the X-Device-ID header against the database to determine device and account status:

  • 410 Gone: Returned when the user no longer exists (account deleted), with code ACCOUNT_DELETED.
  • 403 Forbidden: Returned when the device row contains a revoked_at timestamp, with code DEVICE_DISCONNECTED.
  • 401 Unauthorized: Returned for generic authentication failures.

These status codes serve as the canonical signals that trigger client-side reset procedures, as documented in docs/delete-account-and-revoke-device.md.

PowerSync Connector and Credentials Invalid Detection

Response Interception and Event Dispatch

The frontend PowerSync connector, located in src/db/powersync/connector.ts, intercepts non-OK responses from the token endpoint and converts HTTP status/body pairs into typed reasons:

// src/db/powersync/connector.ts
export const powersyncCredentialsInvalid = 'powersync_credentials_invalid';
export type CredentialsInvalidReason =
  | 'account_deleted'
  | 'device_revoked'
  | 'device_id_taken'
  | 'device_id_required';

The helper function handleCredentialsInvalidIfNeeded (lines 23-45) evaluates responses and dispatches a custom browser event when a reset reason is detected:

if (reason) {
  window.dispatchEvent(
    new CustomEvent(powersyncCredentialsInvalid, { detail: { reason } })
  );
}

This event-driven architecture decouples the network layer from the UI reset logic, allowing multiple components to react to credential invalidation.

Frontend Reset Flow and Listener Implementation

The Central Credentials Invalid Listener

The hook usePowerSyncCredentialsInvalidListener in src/hooks/use-powersync-credentials-invalid-listener.ts provides the single implementation for handling account deletion and device revocation. It subscribes to two distinct data sources:

  1. Event-driven path: Listens for the powersync_credentials_invalid custom event.
  2. Table-driven path: Watches the synced devices table for local changes indicating revoked_at or row deletion.

The hook must be mounted inside AuthProvider before any early-return conditional rendering to ensure it remains active even after the database wipe triggers component unmounting.

Performing the Reset

When triggered, the listener executes performCredentialsInvalidReset (lines 17-20), which orchestrates the two-stage cleanup:

  1. clearLocalData(): Defined in src/lib/cleanup.ts, this function disables PowerSync synchronization, clears localStorage (removing the auth token and device ID), and deletes the local SQLite database.
  2. window.location.replace: Performs a full page reload to a clean signed-out route (/ or /account-deleted), ensuring no stale React state persists.

This guarantees that every client—whether actively syncing or idle—reaches a consistent signed-out state without manual user intervention.

End-to-End Flow Examples

Account Deletion Scenario

When a user deletes their account through the settings interface:

// Example: Delete account button handler
async function deleteAccount() {
  const token = localStorage.getItem('auth_token');
  await fetch(`${process.env.REACT_APP_BACKEND_URL}/v1/account`, {
    method: 'DELETE',
    headers: { Authorization: `Bearer ${token}` },
  });
  // Backend hard-deletes the user; other devices receive 410 on next token request
}

Other active devices detect the deletion on their next PowerSync token refresh, receive the 410 Gone response, and trigger the full reset flow.

Device Revocation Scenario

When a user revokes a specific device from another session:

// Example: Revoke another device by its UUID
async function revokeDevice(deviceId: string) {
  const token = localStorage.getItem('auth_token');
  await fetch(
    `${process.env.REACT_APP_BACKEND_URL}/v1/account/devices/${deviceId}/revoke`,
    {
      method: 'POST',
      headers: { Authorization: `Bearer ${token}` },
    }
  );
  // The revoked device will see `revoked_at` via PowerSync sync or a 403 response
}

The revoked device immediately sees the revoked_at timestamp in its local devices table (triggering the table-driven reset) or receives a 403 Forbidden on its next API call (triggering the event-driven reset).

Using the Credentials Invalid Listener

Implement the reset handling in your application root:

import { AuthProvider } from '@/contexts';
import { usePowerSyncCredentialsInvalidListener } from '@/hooks/use-powersync-credentials-invalid-listener';

function App() {
  usePowerSyncCredentialsInvalidListener(); // must be called inside AuthProvider
  return (
    <AuthProvider>
      {/* ...rest of the component tree */}
    </AuthProvider>
  );
}

Key Implementation Files

Understanding how Thunderbolt handles account deletion and device revocation requires examining these specific source files:

Summary

Thunderbolt guarantees clean client state during account deletion and device revocation through a coordinated three-layer architecture:

  • Backend enforcement via specific HTTP status codes (410 Gone for deleted accounts, 403 Forbidden for revoked devices) returned by the PowerSync token endpoint.
  • Event abstraction where the PowerSync connector converts HTTP errors into browser events (powersync_credentials_invalid) with typed reasons (account_deleted, device_revoked).
  • Unified reset flow executed by usePowerSyncCredentialsInvalidListener, which clears local data via clearLocalData() and performs a full page reload to ensure no stale state persists.

This design ensures that whether a user deletes their account or revokes a specific device, all affected clients immediately enter a consistent signed-out state without requiring manual intervention.

Frequently Asked Questions

What happens when a user deletes their account in Thunderbolt?

When a user deletes their account, the backend hard-deletes all user data via DELETE /v1/account and returns HTTP 200. Other devices that remain active will receive an HTTP 410 Gone response on their next PowerSync token request, triggering an automatic reset that clears local data and reloads the page to a signed-out state.

How does a revoked device know it has been disconnected?

A revoked device detects its status through two mechanisms. First, the PowerSync connector in src/db/powersync/connector.ts intercepts HTTP 403 Forbidden responses with code DEVICE_DISCONNECTED from the token endpoint and dispatches a powersync_credentials_invalid event. Second, the usePowerSyncCredentialsInvalidListener hook monitors the local devices table for changes to the revoked_at column, triggering the same reset flow immediately upon detection.

What is the purpose of the powersync_credentials_invalid event?

The powersync_credentials_invalid event decouples network error handling from UI reset logic. Defined in src/db/powersync/connector.ts, this custom browser event carries a typed reason (account_deleted, device_revoked, etc.) that allows the usePowerSyncCredentialsInvalidListener hook to execute a consistent, application-wide reset without requiring individual components to parse HTTP status codes or manage cleanup logic.

Where does the actual data cleanup occur when credentials become invalid?

The actual cleanup occurs in src/lib/cleanup.ts within the clearLocalData() function. This utility disables PowerSync synchronization, removes the authentication token and device ID from localStorage, and deletes the local SQLite database. After clearLocalData() completes, the usePowerSyncCredentialsInvalidListener hook performs a full page reload via window.location.replace to ensure the application starts from a completely clean state without stale React context or cache.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →