Skip to content
WebViewGold Vibe Coding to App Store
English
Get WebViewGold

Security

API keys and secrets in AI-built apps: what a native wrapper does not hide, and how to review any agent's code

Wrapping a web app in a native shell does not hide its JavaScript: the same bundle loads from your URL, and anyone can open it in a browser. So the web's rules apply. Publishable keys (Supabase publishable or anon, Stripe pk_, Firebase web config, OneSignal App ID) may ship; secret keys (Supabase secret or service_role, Stripe sk_, OneSignal App API keys, service accounts, LLM API keys) never may. Here is how to tell them apart, why row-level security is the real lock, and a review routine that works with any coding agent.

From $109 one-time per platform, or let our team do it all from $698.

  • Approval guarantee – or money back within 14 days
  • No coding needed
  • One-time payment, free updates
  • 24/7 human support
  • Made in Germany since 2014 · ISO 27001

Why the native wrapper does not protect your keys

A WebViewGold app is a native shell around your deployed URL. The JavaScript your users run is downloaded from your server, exactly as in Safari or Chrome. Anyone can open your domain on a laptop, open the developer tools and read every string in the bundle, including environment variables your build tool inlined. The app store listing changes nothing about that.

The native project is not a vault either. Values in Config.swift, Config.java, Info.plist, google-services.json and the custom.js and custom.css files end up inside the IPA or APK, and an app package is an archive that tools unpack in seconds. Treat everything shipped to a device as public. Keys designed for clients are fine with that; secret keys are not.

Build tools make the mistake easy. Variables prefixed VITE_ (Vite) or NEXT_PUBLIC_ (Next.js) are inlined into the client bundle by design. An agent that "fixes" a 401 error by moving a key into such a variable, or by pasting it into a fetch call, has just published it. Agents do this because it makes the error go away; it is your review that has to catch it.

Which keys may ship, and which never

ServiceFine in client codeServer onlyWhat actually protects you
SupabasePublishable key (sb_publishable_...) or the legacy anon keySecret key (sb_secret_...) or the legacy service_role key; both bypass row-level securityRLS policies on every table and storage bucket
FirebaseWeb config (apiKey, authDomain, projectId, appId)Service account JSON, Admin SDK credentialsSecurity Rules, App Check, API key restrictions in Google Cloud
StripePublishable key (pk_live_...)Secret key (sk_live_...), restricted keys (rk_live_...), webhook signing secret (whsec_...)Create Checkout Sessions and PaymentIntents on your server; verify webhooks
OneSignalApp ID (the value in Config.swift / build.gradle)App API key (os_v2_app_...), legacy REST API key, Organization API keySend pushes from your server to the subscription IDs you stored
RevenueCatPublic app-specific SDK key, the kind a native app usesSecret API key for REST callsEntitlements checked on the server via webhooks
LLM APIs (Gemini, OpenAI, Anthropic)NothingEvery API keyA server or edge function that authenticates users and rate-limits

The pattern agents get wrong most often

Three versions of the same mistake, then the fix. The fix always looks the same: the client calls your own endpoint with the user's session, and only the server holds the secret.

javascript
// WRONG: service_role key in the browser. Bypasses every RLS policy.
const admin = createClient(SUPABASE_URL, import.meta.env.VITE_SUPABASE_SERVICE_ROLE_KEY);

// WRONG: Stripe secret key in a fetch from the client
await fetch("https://api.stripe.com/v1/checkout/sessions", {
  method: "POST",
  headers: { Authorization: "Bearer " + import.meta.env.VITE_STRIPE_SECRET_KEY }
});

// WRONG: sending pushes from the browser with the OneSignal App API key
await fetch("https://api.onesignal.com/notifications", {
  method: "POST",
  headers: { Authorization: "Key " + ONESIGNAL_APP_API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ app_id: ONESIGNAL_APP_ID, contents: { en: "Hi" } })
});

// RIGHT: the client calls your endpoint with the user's session token
const res = await fetch("/api/checkout", {
  method: "POST",
  headers: { Authorization: "Bearer " + session.access_token, "Content-Type": "application/json" },
  body: JSON.stringify({ priceId: "price_basic_monthly" })
});

// RIGHT: on the server (API route, Supabase Edge Function, Cloud Function)
// 1. verify the session token and derive the user id from it, never from the body
// 2. read STRIPE_SECRET_KEY / ONESIGNAL_APP_API_KEY / SUPABASE_SECRET_KEY from server env
// 3. call the provider, return only what the client needs

Row-level security is the real lock

A publishable Supabase key is safe only because Postgres decides what each request may see: with no signed-in user it runs as anon, with one as authenticated, and policies filter rows. A secret key runs as service_role, which has BYPASSRLS (Supabase API keys docs). Every table in the exposed schema needs RLS plus policies like these; Firebase achieves the same with Security Rules. The Supabase checklist and the Firebase checklist cover the rest of the backend.

text
-- Supabase SQL: enable RLS and let users see and change only their own row
alter table public.profiles enable row level security;

create policy "profiles: read own" on public.profiles
  for select to authenticated
  using ((select auth.uid()) = id);

create policy "profiles: update own" on public.profiles
  for update to authenticated
  using ((select auth.uid()) = id)
  with check ((select auth.uid()) = id);

-- Firestore rules, same idea:
-- match /profiles/{uid} {
--   allow read, write: if request.auth != null && request.auth.uid == uid;
-- }

How to review any agent's code for leaked secrets

Works the same for Claude Code, Codex, Cursor, Gemini CLI, Antigravity, Kiro, Cline and every app builder.

  1. Scan the built bundle, not the source

    Run the production build and search the output folder (scan below). Source code can look clean while an inlined environment variable sits in the bundle.

  2. Check every public prefix

    List all variables starting with VITE_, NEXT_PUBLIC_ or similar. Each one must be something you would print on a billboard.

  3. Check the native projects

    Config.swift, Config.java, Info.plist, build.gradle, custom.js and the Firebase config files may contain public IDs only (OneSignal App ID, RevenueCat public key, AdMob IDs, Firebase config).

  4. Check the backend

    Supabase: RLS enabled on every table, no policy that is simply true for writes, Security Advisor warnings resolved. Firebase: no if true rules. Edge functions derive the user from the token, never from a user ID in the request body.

  5. Check history and rotate

    A key that was ever committed or deployed is leaked, even if the latest commit removed it. Rotate it in the provider dashboard first, then clean up. Turn on secret scanning and push protection for the repository.

  6. Teach the agent

    Add a secrets section to your context file so the next session does not repeat it: CLAUDE.md, AGENTS.md, GEMINI.md, .clinerules or Kiro steering, depending on your coding agent.

A two-minute leak scan after every build

Adjust the folders to your stack (Vite: dist/, Next.js: .next/static/, Create React App: build/). A match on AIza is expected for a Firebase web config; confirm that key is restricted and is not a Gemini key.

bash
# 1. Build the production bundle
npm run build

# 2. Search shipped files for secret-looking strings
grep -rEn "sk_live_|sk_test_|rk_live_|whsec_|sb_secret_|os_v2_app_|BEGIN (RSA )?PRIVATE KEY|AIza[0-9A-Za-z_-]{35}" dist/ .next/static/ build/ 2>/dev/null

# 3. Legacy Supabase keys are JWTs: decode each one and look at "role"
#    "anon" is expected, "service_role" means a secret key shipped
grep -rhoE "eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+" dist/ 2>/dev/null | sort -u | while read -r t; do
  echo "$t" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null; echo
done

# 4. Native projects and git history
grep -rEn "sk_live_|sb_secret_|os_v2_app_|service_role" ios/ android/ 2>/dev/null
git log -p --all | grep -En "sk_live_|sb_secret_|os_v2_app_|SERVICE_ROLE" | head -20

Security review prompt for any coding agent

It asks for a report first and never prints full secrets, so you can run it even in a shared session.

Paste into your agent in the project root
Do a secrets and data-access review of this repository before we publish it as iOS and Android apps with WebViewGold. Remember: the apps load our deployed web app, so every file in the client bundle is public, and every value in ios/ and android/ ships inside the app package.

1. List every API key, token, secret and credential referenced anywhere: source, env files, config, CI workflows, ios/, android/. For each: service, where it is used, whether it reaches the client bundle, and whether it is publishable (Supabase publishable or anon, Stripe pk_, Firebase web config, OneSignal App ID, RevenueCat public SDK key) or secret (Supabase secret or service_role, Stripe sk_, rk_ or whsec_, OneSignal App API key, service accounts, any LLM API key).
2. For each secret that reaches the client: propose moving the call into a server route or function that verifies the user's session first and reads the secret from server-side environment variables. Show me the plan before changing any payment code.
3. Supabase: list every table and storage bucket, whether RLS is enabled and which policies exist. Flag tables without RLS and policies that do not check auth.uid(). Firebase: flag rules that allow access without request.auth or across users.
4. Edge functions and API routes: flag any that take a user id from the request body instead of the verified token.
5. Git history: find committed .env files or keys and list which keys must be rotated. Do not rewrite history.
6. Add a short secrets section to our agent context file that names the allowed publishable keys and forbids everything else in client code.

Report as a table. Never print full secret values; show only the first 6 characters.

Pitfalls that survive a quick review

  • "It is an app, nobody can see the code." The code is on your URL and in the package; both are public.
  • Obfuscation instead of removal. Base64, string splitting or minification do not protect a key; anyone who runs the app can watch it being used.
  • An "admin page" in the same bundle that uses the service role key. Admin features need their own server-side endpoints with role checks.
  • A delete-account or push endpoint that trusts a user ID from the request. Anyone can then delete or message anyone; derive the user from the verified token.
  • Paid features unlocked by a client-side flag. Check entitlements on the server; the RevenueCat guide shows the webhook pattern.
  • Deleting a leaked key from the code without rotating it. The old value still works for whoever copied it.

Release checklist

  • The leak scan finds no secret prefixes in the build output, the native projects or the git history.
  • Every Supabase table and bucket has RLS; Firebase rules have no if true.
  • Payments, pushes, LLM calls and account deletion run through server code that verifies the user.
  • Public Google API keys are restricted to the APIs they need; Gemini has its own server-side key.
  • OneSignal pushes are sent from your server with the App API key, using the subscription IDs the apps report (see the push notification docs).
  • Any key that ever leaked has been rotated, not just removed.

Frequently asked questions

Is the Supabase anon key safe in my app?

Yes, it is designed for public clients, as is the newer publishable key, provided row-level security is enabled with sensible policies on every table. Supabase plans to deprecate the legacy anon and service_role keys by the end of 2026, so new projects should use publishable and secret keys.

Can someone extract keys from my iOS or Android app?

Yes. The web bundle downloads from your URL and can be read in any browser, and IPA and APK files are archives whose configuration files and strings are easy to inspect. Only ship keys that are meant to be public.

Is the Firebase apiKey a secret?

Not by design; it identifies your project, and Security Rules and App Check do the protecting. Since unrestricted Google API keys can also reach the Gemini API once it is enabled in the project, restrict the key to the APIs it needs and keep Gemini calls on the server.

Which OneSignal value goes into the WebViewGold app?

Only the public App ID, in Config.swift on iOS and in build.gradle on Android. The App API key that sends notifications stays on your server, which targets users through the subscription IDs the app passes to your web app.

I leaked a key. What now?

Rotate it in the provider dashboard immediately, check the provider logs and billing for misuse, then remove it from the code and move the call server-side. Removing it without rotating leaves the old key valid.

Will Apple or Google reject my app for an exposed key?

Store review is not a security audit, so do not count on it to catch leaks. The real risks are abuse, unexpected bills and exposed user data, which can turn into a privacy problem with both stores.

Three ways to get your app into the stores

Same result, different amount of work on your side. All three come with the approval guarantee and 24/7 human support.

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 page

Cloud Builder

$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

Full Service

$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 by WeSetupYourWebViewApp

Winter Sale prices valid until December 31, 2026.

Ready to ship your app?

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.