Implementing Secure Account Deletion in Expo and Supabase
Account deletion in an Expo app is a controlled workflow: confirm intent, authenticate the caller, clean up data and providers, then remove the identity safely.
Deleting an account is not a client-side deleteUser() button. It is a destructive workflow spanning four separate responsibilities:
- Client UI collects deliberate consent and shows honest progress.
- An authenticated backend operation identifies the requesting user and applies product rules.
- Privileged identity deletion removes the Supabase Auth user using a secret that never reaches the app.
- Data cleanup removes, anonymizes, or retains data according to its ownership and retention rules.
Keeping those boundaries explicit makes the flow easier to audit and safer to retry. There is no universal deletion order: foreign keys, shared records, Storage ownership, subscriptions, and legal retention all change the correct design.
Sign out is not deletion
Sign-out removes the local session and perhaps user-scoped cached data. The person can sign in again; their Auth identity and server data remain. Account deletion removes access and processes the account's associated data under the product's policy. A button that only clears SecureStore is therefore not account deletion.
Apple requires apps that support account creation to let people initiate deletion in the app. Its guidance also permits reasonable reauthentication and confirmation safeguards, provided they do not turn deletion into an obstacle course. If deletion is asynchronous, say what happens next and when the person will receive confirmation. Apple's account-deletion guidance is the relevant review reference.
Make intent and recent authentication visible
Put deletion in account settings, explain the consequences, and require an explicit confirmation such as entering DELETE. For a sensitive action, require recent authentication rather than trusting a session restored weeks ago. The exact mechanism depends on the sign-in method: an email flow may ask for a fresh password or one-time code; Apple or Google may run a fresh provider flow. Do not mistake a profile email for proof that the person is currently in control of the account.
The Expo screen should initiate the request, never perform the privileged work:
async function requestDeletion() {
// The user has already completed a confirmation and recent-authentication step.
const { data, error } = await supabase.functions.invoke("delete-account", {
body: { confirmation: "DELETE", requestId: crypto.randomUUID() },
});
if (error) throw error;
return data;
}supabase.functions.invoke sends the signed-in user's JWT. The function must still validate the request, derive the user ID from verified credentials rather than its JSON body, and apply its own authorization and rate limits. A client-provided userId is an input, not authority.
Keep privileged keys on the server
Supabase's admin user deletion API needs the project secret/service-role capability. It bypasses Row Level Security and must only run in developer-controlled server code, such as an Edge Function or backend endpoint. Never put it in an Expo bundle, an EXPO_PUBLIC_ variable, or a mobile configuration file. Supabase documents this boundary for admin deletion and API keys.
This simplified function shows the separation, not a complete retention policy:
// Simplified server-side pseudocode.
export async function deleteAccount(request: Request) {
const user = await requireRecentAuthenticatedUser(request);
const requestId = await parseAndValidateRequestId(request);
const existing = await findDeletionJob(user.id, requestId);
if (existing) return Response.json(existing.result); // safe retry
await createDeletionJob({ userId: user.id, requestId, state: "pending" });
await deleteOrAnonymizeOwnedData(user.id);
await deleteStorageObjectsOwnedBy(user.id);
await revokeExternalRelationshipsIfRequired(user.id);
await supabaseAdmin.auth.admin.deleteUser(user.id);
await finishDeletionJob(user.id, requestId);
return Response.json({ deleted: true });
}Model the data, not just the user row
Inventory every data class before implementing this handler. Rows wholly owned by a user can often use foreign keys with ON DELETE CASCADE; do not add cascades blindly. A shared document, marketplace record, aggregate, abuse-prevention record, invoice, or legally retained record may need an author reference removed, a tombstone, anonymization, or a separate retention schedule instead.
Supabase notes a practical ordering constraint: an Auth user that owns Supabase Storage objects cannot be deleted until those objects are deleted or reassigned. That makes storage an explicit cleanup stage, not an afterthought. Consider thumbnails, uploads, derived files, and object metadata, not only database rows. See Supabase's user-data management guidance.
Subscriptions are another separate relationship. Deleting an application account does not automatically cancel an Apple subscription. Explain billing and cancellation before confirmation, and point iOS users to subscription management where appropriate. If Sign in with Apple created the identity, Apple says to revoke associated tokens during deletion when available; lack of a token does not remove the obligation to delete the account data. Apple TN3194 covers that provider-specific step. Google and other providers have their own token and consent lifecycles; only call provider cleanup that your integration actually needs.
Treat failures as normal states
A multi-system deletion cannot be made magically atomic. A provider revocation can succeed while a database cleanup fails, or the Auth user can be removed after a worker times out before recording success. Use a durable deletion job with a stable request ID, stage/status, timestamps, and limited error information. Retrying the same request should return the known outcome or resume unfinished cleanup, not start a second deletion.
Database transactions protect work inside one database. They do not roll back a completed external API call. For external cleanup, record intent first, make calls idempotent where possible, and run retryable reconciliation jobs. Do not log raw tokens, emails, or deleted content merely to debug the workflow. Keep only the audit information your security and retention policy genuinely needs, then apply retention to that audit trail too.
After a confirmed deletion, clear app-owned user data, sign the device out, and return to the signed-out UI. Deleting a Supabase user prevents new refresh tokens, but already issued stateless access tokens can remain valid until expiry; sensitive APIs can use short lifetimes and stronger session checks where that residual window matters.
Re-creation is a new decision
A person may later create a new account with the same provider. Whether it can recover prior data depends on the retention policy; do not promise restoration after deletion unless you intentionally keep recoverable data and tell the user. Soft deletion, anonymization, legal retention, and immediate hard deletion each answer different product and regulatory needs. Choose and document one for each data category rather than assuming one database command satisfies all of them.
Testing checklist
- Fresh and expired sessions, plus a failed reauthentication.
- Double taps, app termination, timeout after server success, and a retry with the same request ID.
- Storage objects, cascading owned rows, shared data, subscription messaging, and provider-specific cleanup.
- A deletion job that fails at each stage and is resumed by a worker.
- Final sign-out, a stale local cache, and later account re-creation.
Choose the order intentionally
One common ordering is to stop new work, mark the deletion job pending, remove owned Storage objects and application rows, revoke provider relationships where required, then delete the Auth identity. That order can preserve the authenticated identity long enough to authorize recovery work, but it may leave a person able to use an existing session while cleanup runs. Another design removes identity access early and finishes cleanup from a trusted worker. That reduces access sooner, but the worker must have a durable job record and enough authority to complete cleanup without the user's token.
Neither order is automatically correct. Write down the failure mode you accept, then test it. For example, records that must be retained for a fraud or tax obligation should be isolated from cascading user-owned data before implementing the cascade. Shared content might be deleted, reassigned, or retained with its personal author data removed depending on the product promise and applicable law. Product, privacy, and legal owners should agree on those answers before code turns a policy ambiguity into irreversible data loss.
For a finished UX, show a non-dismissible progress state only while the initiating request is active, then communicate whether deletion completed immediately or is queued. Do not tell the person that all data is gone until the job says so. A confirmation email or in-app status can be useful for asynchronous workflows, but it should contain no sensitive deleted data.
The authentication foundation is covered in Apple and Google Sign-In with Expo and Supabase. Pair this workflow with production-state testing before release: a destructive flow is only complete when failure and recovery are designed too.
Related Tutorials
A production-minded approach to social sign-in in Expo: native Apple credentials, OAuth redirects, Supabase sessions, secure storage, and the identity edge cases that affect real users.
How to add in-app purchases to an Expo app with RevenueCat: entitlements over product IDs, why restore is mandatory, and why the client is the wrong place to trust a purchase.