A0.DEV SECURITY CHECKLIST
a0.dev is an AI mobile app generator that produces React Native / Expo apps from natural-language prompts, with live preview on your phone and iteration via chat. Backends are typically wired to Supabase or Firebase, or to generated API endpoints. The most common security issues come from one core misunderstanding: the mobile client is not a trust boundary. Everything in the shipped binary — keys, config, premium logic — is extractable, so the backend rules (Supabase RLS, Firestore rules, endpoint auth) are the only real permission system. The checklist below is what we look for first when we audit an AI-generated Expo app.
Treat Critical as launch-blocking. High is week-one. Medium is the cleanup once you have users.
How to use this checklist
Walk it once against the generated source and the backend project, with a second test account ready. After each fix, sign in as user B and try to read user A’s data — directly against the backend, not through the app UI. After the whole list passes, run a scan against the deployed API.
Critical (fix before launch)
1. Write backend rules for every table or collection
Why it matters. The generated app can behave perfectly while the Supabase project behind it has RLS disabled or the Firestore rules allow open reads. The anon key and Firebase config ship in the bundle by design — anyone can extract them and query the backend directly, skipping every check the React Native code makes.
How to check. For Supabase: confirm RLS is enabled on every table and each has explicit policies. For Firebase: read the Firestore/Storage rules files; allow read, write: if true (or if request.auth != null on user-scoped data) is a finding. Then query the backend with only the client config from the bundle and no session.
How to fix. Deny by default. Per-table/collection policies scoped to auth.uid() == user_id (or the Firestore equivalent). “Any authenticated user” is not a scope for personal data.
2. Remove server-side secrets from the bundle
Why it matters. Any key in the app bundle is extractable from the released binary. Generated code sometimes calls OpenAI, Stripe, or a service-role client directly from the app, embedding the secret. Unlike a web deploy, you cannot pull the release back — installed builds keep the key forever.
How to check. Search the source and the exported bundle for sk_, sk-, AIza (fine only as a restricted Firebase key), service_role, and any provider secret prefix. Also check app.json / app.config.js extra fields — those are bundled, not secret.
How to fix. Move every secret-requiring call behind a server endpoint or edge function that holds the key. Rotate anything that ever shipped in a build.
3. Add auth and ownership checks to every generated endpoint
Why it matters. Generated API endpoints and edge functions frequently ship with no auth check, or with auth but no ownership check — any signed-in user can fetch any record by ID. This is the classic BOLA in AI-generated CRUD shape, and on mobile the endpoint URLs are trivially discoverable from the bundle.
How to check. List every endpoint the app calls (grep the source for fetch/axios calls). Hit each without a token, then with user B’s token requesting user A’s resource ID. Expect 401 and 403/404 respectively.
How to fix. Validate the session server-side on every endpoint, then check the requested resource belongs to the caller before returning it.
4. Move tokens out of AsyncStorage
Why it matters. Generated auth flows commonly persist session and refresh tokens in AsyncStorage — unencrypted files on the device, readable via device access, backups, or other-app exploits. The platform provides hardware-backed storage that the generator does not use by default.
How to check. Grep for AsyncStorage.setItem with token/session/auth-shaped keys, and check the storage adapter passed to the Supabase or Firebase client.
How to fix. Use expo-secure-store (Keychain/Keystore) for anything session-shaped, including as the storage adapter for supabase-js.
5. Enforce premium and role gating server-side
Why it matters. if (isPremium) in React Native code is decoration. Attackers patch the binary, tamper with responses, or call the premium endpoint directly. If the server never checks the entitlement, the paid tier is free for anyone technical.
How to check. Find every premium-gated feature and identify the backend call behind it. Call that backend directly with a free-tier account’s token. If it responds, the gate is client-side only.
How to fix. The endpoint or backend rule checks the subscription (server-stored flag, validated store receipt, or RevenueCat entitlement) before serving. Same for admin/role checks.
6. Block privileged-field writes
Why it matters. If users can update their own profile row and that row contains role, is_premium, or credits, a direct API call can set them. Generated update paths rarely restrict columns.
How to check. As a normal user, PATCH your own record with {"role": "admin"} or {"is_premium": true} directly against the backend. Any 200 that persists the value is a finding.
How to fix. Column-level security or a server-mediated write path that strips privileged fields. Privileged flags change only via server-side logic.
High (fix in the first week)
7. Re-validate input at the server boundary
Generated forms validate in the UI only. Every endpoint and edge function re-validates body, params, and query server-side — a direct API call never ran the client schema.
8. Lock OAuth redirects and deep links
Expo custom URL schemes can be claimed by other apps on-device. Use universal links / app links for production OAuth callbacks, restrict redirect URIs in the provider console to your real scheme/domain, and validate state. See SSRF / open redirect / OAuth.
9. Add rate limiting on auth and expensive endpoints
Neither generated endpoints nor default backend configs throttle per-IP. Auth, password reset, and any LLM-calling endpoint need limits before someone scripts them.
10. Strip logging from release builds
Generated code logs request and response bodies. Remove console.log from production bundles (babel-plugin-transform-remove-console) — on Android, logs are readable with adb access.
11. Verify third-party webhook signatures
If payments or notifications flow through generated webhook handlers (Stripe, RevenueCat), confirm each verifies the provider signature instead of trusting the body.
12. Audit dependencies for hallucinated packages
AI-generated package.json entries occasionally name packages that don’t exist or are typosquats. Verify each dependency is the real package. See the package hallucination scanner.
Medium (fix when you can)
13. Consider certificate pinning for sensitive traffic
Generated Expo apps trust any device-trusted CA — no pinning by default. For apps handling sensitive data, add pinning via a config plugin. Never ship with TLS verification disabled.
14. Disable email enumeration in the auth flow
Signup, password reset, and magic-link responses should be identical for existing and non-existing accounts.
15. Cap file upload size and types
If the app uploads to Supabase Storage or Firebase Storage, enforce size and MIME limits in the storage rules, not just in the picker UI.
16. Remove test accounts and seed data
Delete demo users and sample rows from the production backend before launch — they often carry weak passwords and known IDs.
17. Monitor for enumeration patterns
Ship backend logs somewhere queryable and alert on bulk sequential reads from a single token or IP.
18. Document each table’s access model
A written record of who can read/write what gives you something to diff after the next AI regeneration changes the schema.
After every a0.dev regeneration
- Diff the backend schema and rules — regeneration can add tables without policies.
- Re-grep the source for newly introduced hardcoded keys.
- Re-test cross-user access directly against the backend (user B reads user A).
- Confirm token storage still uses SecureStore, not a regenerated AsyncStorage default.
- Check
package.jsonfor new, unverified dependencies.
Common attack patterns we see in AI-generated Expo apps
The extracted-config dump. Attacker unzips the APK, pulls the Supabase URL and anon key, and queries tables that have no RLS. The app’s permission checks never run.
The un-rotatable key. A secret key shipped in v1.0. It gets rotated after the leak — and every user still on v1.0 breaks, while archived copies of the binary keep the old key forever.
The free premium tier. Premium content endpoint checks nothing; a free user replays the request the paid UI would have made and gets the content.
The AsyncStorage session lift. Device backup extraction yields plaintext refresh tokens; the session works from the attacker’s machine.
Related Resources
How to Secure a0.dev
Step-by-step guide for hardening an a0.dev app — backend rules, secret handling, token storage, and endpoint auth in long form.
Is a0.dev Safe?
In-depth analysis of what AI-generated mobile apps ship with by default — what’s locked down, what’s open, and what we find at launch.
Automate Your Checklist
A checklist tells you what to look for. A scanner tells you what’s actually broken right now. VibeEval probes the backend your a0.dev app talks to, attempts the cross-user BOLA and privileged-write attacks above, and reports what got through — with the table, rule, or endpoint to fix.
SCAN YOUR A0.DEV APP
14-day trial. No card. Results in under 60 seconds.