Do it yourself
$109 per platform, one-time
The WebViewGold license: a finished Xcode or Android Studio project. Paste your URL, set name and icon, switch on features, build and submit. Free updates forever.
See plans on the main pageChatGPT & Codex
Both stores reject apps that let people sign up but not leave. The feature looks small, but a correct deletion touches auth, every collection or table, file storage, subscriptions and the Sign in with Apple token, which is exactly the kind of cross-cutting change Codex handles well once it has an inventory. Here is a two-pass workflow: Codex maps the data in a read-only session, then implements the purge with tests.
From $109 one-time per platform, or let our team do it all from $698.
Apple (Guideline 5.1.1(v)): if the app supports account creation, people must be able to start account deletion inside the app, and deletion means the account and its data, not a deactivated flag. A confirmation step is fine. If people can sign in with Apple, Apple expects you to revoke their tokens through the Sign in with Apple REST API when they delete. Google Play: an in-app path plus a web page where people can request deletion without reinstalling the app, whose URL you enter in the Data safety section of the Play Console.
What the reviewer actually does: signs up or uses your demo account, opens settings, looks for Delete account, and sometimes goes through with it. On Google Play, the web link gets opened too; if it needs a login or returns a 404, that is a rejection (the Play rejection explained).
Why two passes with Codex: deletion fails quietly. A forgotten collection or storage folder throws no error, it simply stays. Mapping the data before writing code is what makes the result complete. The examples use React, Express, Firebase Auth, Firestore and Cloud Storage; the Supabase equivalents are in the table. The account deletion requirement guide covers the same rules for builders without a coding agent, and the isNativeApp() helper comes from the AGENTS.md template.
The read-only sandbox lets Codex read files and run harmless commands such as rg, but not change anything. That keeps the first pass honest: it can only look.
# Pass 1: inventory only. Codex can read, search and answer, but not edit.
cd my-app
codex --sandbox read-only --ask-for-approval on-request
# Paste this into the session:
# Read AGENTS.md. Find every place this codebase stores or sends data about a
# user: auth providers, Firestore collections and subcollections (and the field
# that links each document to the user), Cloud Storage paths, server logs,
# third parties that receive user data (RevenueCat, Stripe, OneSignal, email,
# analytics, error tracking) and what the web app keeps on the device
# (cookies, localStorage, IndexedDB, values written with writevalue://).
# Output one Markdown table: location | link to the user | delete or retain | reason.
# Mark anything you are unsure about. Do not propose code yet.
# When the table is right, switch to write access inside the session with
# /permissions and ask Codex to save it as docs/user-data-map.md. Commit it.| Location | Firebase (Express API) | Supabase |
|---|---|---|
| Auth user | getAuth().deleteUser(uid) with firebase-admin, as the last step | supabase.auth.admin.deleteUser(id) in an Edge Function with the service role key, as the last step |
| Database | recursiveDelete() for the user document and its subcollections, plus queries on owner fields | Foreign keys to auth.users with on delete cascade, or explicit deletes before the auth call |
| Files | getStorage().bucket().deleteFiles({ prefix: "users/UID/" }) | List the user's folder, then storage.from(bucket).remove(paths) |
| Sign in with Apple | revokeAccessToken(auth, token) after the user re-authenticates with Apple; Firebase stores no Apple tokens | Not automatic. Apple's revocation endpoint needs a token from the sign-in; have Codex check what your flow exposes and flag the gap |
| Store subscriptions | You cannot cancel them for the user. Say so in the dialog and offer cancelinapppurchase://; optionally delete the RevenueCat customer server-side | Same |
| Push, email, analytics | Delete the push device rows, remove the contact from your email tool, use each vendor's deletion API | Same |
| The device | reset:// makes WebViewGold clear cookies and cache; overwrite anything stored with writevalue://, which survives cache clears | Same |

WebViewGold has allowed us the shortest and most efficient time-to-market ever possible from among all the other app platforms we have ever tried during the past 5 years. I don’t know why I hadn’t discovered you sooner, but I am really, really glad I did. Currently nearing 6000 app downloads on both iOS App Store & Google Play Store without any problem!
Start a fresh session with codex --sandbox workspace-write --ask-for-approval on-request so the map, not the previous conversation, is the source of truth. For Supabase, replace step 3 with an Edge Function that verifies the JWT, removes storage objects, relies on cascading foreign keys and calls auth.admin.deleteUser.
Read AGENTS.md and docs/user-data-map.md. Implement account deletion that satisfies App Store Guideline 5.1.1(v) and Google Play's account deletion policy. Stack: React front end, Express API, Firebase Auth, Firestore, Cloud Storage. This web app also runs inside native iOS and Android apps built with WebViewGold; use isNativeApp() for app-only behavior.
1. Settings > Account > Delete account. A confirmation sheet explains what is deleted, what we retain and for how long (from the map), and that store subscriptions are billed by Apple or Google until cancelled. If the user has an active store subscription and isNativeApp() is true, show a Manage subscription button that navigates to cancelinapppurchase://.
2. Recent login: if the server answers recent-login-required, re-authenticate. For users with the apple.com provider, always re-authenticate with OAuthProvider("apple.com"), read the access token with OAuthProvider.credentialFromResult, and call revokeAccessToken(auth, token) before the server call.
3. DELETE /api/account in Express: verify the ID token with checkRevoked, require auth_time within the last 5 minutes, then delete in this order: Storage files under users/{uid}/, every Firestore location from the map (recursiveDelete for subcollections, including pushDevices), the Stripe customer and, if the map says so, the RevenueCat customer (behind a feature flag, secret from the environment), and finally the Auth user. Every step must be safe to re-run if the request fails halfway.
4. After success: sign out, and when isNativeApp() is true navigate to reset:// so WebViewGold clears cookies and cache; in browsers, clear localStorage. Then show a goodbye screen.
5. Public /delete-account page that works without login: it explains the in-app steps and offers a request form (email address) for people who no longer have the app. Requests go to a deletionRequests collection and trigger an email to [support address].
6. Add a short section on deletion and retention to /privacy.
7. Tests: use the Firebase emulators if the project already uses them, otherwise mocks. Deletion removes every location in the map, a second call is a no-op, and a stale token is refused.
Report the changed files, the Play Console URL to enter, and what I must configure.Two files carry the logic. The order on the server matters: data first, auth user last, so a failed request can simply be retried. The client handles the Apple revocation, because Firebase does not keep Apple tokens and the user has to re-authenticate anyway.
// server/routes/account.js (Express + firebase-admin)
import express from "express";
import { getAuth } from "firebase-admin/auth";
import { getFirestore } from "firebase-admin/firestore";
import { getStorage } from "firebase-admin/storage";
export const accountRouter = express.Router();
const OWNED = [["projects", "ownerId"], ["comments", "authorId"], ["pushDevices", "uid"]]; // from docs/user-data-map.md
accountRouter.delete("/api/account", async (req, res) => {
const idToken = (req.get("Authorization") || "").replace(/^Bearer /, "");
let user;
try {
user = await getAuth().verifyIdToken(idToken, true); // true = reject revoked tokens
} catch {
return res.status(401).json({ error: "unauthenticated" });
}
if (Date.now() / 1000 - user.auth_time > 300) {
return res.status(403).json({ error: "recent-login-required" });
}
const uid = user.uid;
const db = getFirestore();
await getStorage().bucket().deleteFiles({ prefix: "users/" + uid + "/" });
for (const [name, field] of OWNED) {
const owned = await db.collection(name).where(field, "==", uid).get();
for (const doc of owned.docs) await db.recursiveDelete(doc.ref);
}
await db.recursiveDelete(db.collection("users").doc(uid));
await getAuth().deleteUser(uid); // last: everything above is safe to re-run
res.status(204).end();
});
// src/lib/deleteAccount.js (client, Firebase JS SDK)
import { OAuthProvider, reauthenticateWithPopup, revokeAccessToken } from "firebase/auth";
import { isNativeApp } from "./platform";
export async function deleteAccount(auth) {
const user = auth.currentUser;
if (user.providerData.some((p) => p.providerId === "apple.com")) {
const result = await reauthenticateWithPopup(user, new OAuthProvider("apple.com"));
const credential = OAuthProvider.credentialFromResult(result);
await revokeAccessToken(auth, credential.accessToken); // Apple 5.1.1(v)
}
const res = await fetch("/api/account", {
method: "DELETE",
headers: { Authorization: "Bearer " + (await user.getIdToken(true)) }
});
if (res.status === 403) throw new Error("recent-login-required");
if (!res.ok) throw new Error("delete-failed");
await auth.signOut();
if (isNativeApp()) window.location.href = "reset://"; // WebViewGold clears cookies and cache
}Run pass 1 in the read-only sandbox, correct the table yourself (Codex cannot know which data you must keep for tax or legal reasons), and commit docs/user-data-map.md.
Run pass 2 in a fresh session. Ask for /review before you accept the changes and read the server route against the map row by row.
Enter the public /delete-account URL in the Data safety section of the Play Console. Mention deletion and retention in your privacy policy (what the policy needs).
Run the checklist below with a throwaway account on a TestFlight build and an internal testing build.
Reviewers may delete it. Recreate it before every submission, and say in the review notes that they are welcome to create a fresh account to test deletion. More on the release path in the Codex workflow guide.
/delete-account page opens without login in a private browser window and the request form arrives.deleted: true flag is not deletion under 5.1.1(v).reauthenticateWithPopup misbehaves in the WebView, use reauthenticateWithRedirect and read the result with getRedirectResult./delete-account page behind login, or a service role key in client code on Supabase. Both get flagged quickly.Yes. You can schedule permanent deletion for a short period later (for example 14 days) if you say so in the confirmation and in the privacy policy, and let the user cancel by signing in. The deletion itself must still happen.
It is a good base: it deletes configured Firestore paths and Storage files when the auth user is deleted. You still need the in-app button, the Apple token revocation, the public request page and a check that its configuration matches your data map.
No. Google Play asks for a way to request deletion outside the app. A form that creates a request you then process is acceptable, as long as you actually process it in the time you state.
Delete or anonymize what you do not need to keep. Payment records may have legal retention periods; list what you keep and why in the privacy policy. Cancelling store subscriptions remains the user's action.
No. Codex writes the code and tests against emulators or mocks; you run the deletion against production through the deployed route. Keep production credentials out of agent sessions and cloud environments.
Same result, different amount of work on your side. All three come with the approval guarantee and 24/7 human support.
$109 per platform, one-time
The WebViewGold license: a finished Xcode or Android Studio project. Paste your URL, set name and icon, switch on features, build and submit. Free updates forever.
See plans on the main page$349 per license, one-time add-on
No Mac, no Xcode, no Android Studio: configure in the browser, we build both apps in the cloud and hand them to App Store Connect and the Play Console.
How the Cloud Builder works Try the free demo$698 per platform $798
Send us your URL and icon. Our team configures, builds and delivers your app within 72 hours, white-label, with source projects. Both platforms for $1,158.
Full Service details Powered byWinter Sale prices valid until December 31, 2026.
App Store & Google Play approval guarantee – or your money back within 14 days
Rather hand it over? Full Service: we configure, build and deliver within 72 hours, from $698 per platform. Questions? Chat with us 24/7.
Refunds are granted under the conditions listed in our FAQ: a non-content rejection by Apple and/or Google, claimed within 14 days after purchase.