How I Would Fix database rules leaking customer data in a React Native and Expo AI chatbot product Using Launch Ready.
The symptom is usually blunt: one user can see another user's chat history, saved prompts, uploaded files, or profile data inside the React Native app. In...
How I Would Fix database rules leaking customer data in a React Native and Expo AI chatbot product Using Launch Ready
The symptom is usually blunt: one user can see another user's chat history, saved prompts, uploaded files, or profile data inside the React Native app. In an AI chatbot product, that is not just a bug. It is a customer trust problem, a support burden, and a possible privacy incident.
The most likely root cause is weak database authorization rules, usually "allow read" rules that are too broad, missing row-level ownership checks, or client code querying shared collections without filtering by tenant or user ID. The first thing I would inspect is the exact path from the Expo app to the database: auth state, request identity, query filters, and the security rules or policies attached to the tables or collections.
Triage in the First Hour
1. Check whether the leak is reproducible with two test accounts.
- Create User A and User B.
- Confirm whether User A can see User B's chats, messages, attachments, or metadata.
- If yes, treat it as a production data exposure until proven otherwise.
2. Inspect the auth token flow in the Expo app.
- Confirm the app sends a valid session token on every request.
- Check whether token refresh is working after logout and login switches.
- Look for stale cached sessions in AsyncStorage or SecureStore.
3. Review database access rules first, not UI code.
- Open the policy file, rules dashboard, or collection permissions.
- Look for wildcard read access, missing `user_id` checks, or public read on private tables.
- Verify whether service-role credentials are exposed anywhere in the client bundle.
4. Check recent deploys and config changes.
- Review the last 24 to 72 hours of commits.
- Look for changes to auth middleware, schema migrations, storage rules, or chatbot message queries.
- Compare production and staging environment variables.
5. Inspect logs and dashboards for unusual access patterns.
- Query logs for cross-user reads on chat tables.
- Check spikes in 200 responses on endpoints that should return 403 or 404.
- Look for requests without auth headers still returning private records.
6. Audit build artifacts from Expo and EAS.
- Confirm no secrets were baked into JS bundles.
- Check whether environment variables were accidentally prefixed for client use when they should have stayed server-only.
- Verify release channel and bundle hash match the intended deployment.
7. Freeze risky changes until containment is done.
- Pause new releases if customer data exposure is active.
- Disable any public sharing feature that might be compounding access issues.
- Notify support so they do not promise that "it is just a display bug."
## Quick checks I would run during triage grep -R "service_role\|admin secret\|SUPABASE_SERVICE_ROLE_KEY\|FIREBASE_ADMIN" . grep -R "select.*\*" app src . grep -R "AsyncStorage\|SecureStore" app src .
Root Causes
| Likely cause | What it looks like | How I confirm it | |---|---|---| | Over-permissive database rules | Private rows readable by any signed-in user | Test with two accounts and inspect policy conditions | | Missing ownership filter in queries | App fetches all chats then filters in UI only | Review network calls and query builders | | Service key exposed in client code | App can bypass row-level security entirely | Search bundle/config for admin keys | | Shared tenant schema without tenant scoping | Data from multiple customers stored together without `tenant_id` enforcement | Inspect schema and foreign keys | | Broken auth state after logout/login | New user inherits old session cache | Reproduce with account switching on one device | | Backend endpoint trusts client-supplied user ID | API accepts arbitrary `user_id` instead of deriving identity from token | Review endpoint handlers and request validation |
The highest-risk pattern is this: the frontend asks for data by `user_id`, and the backend trusts that value. That turns identity into user input, which means anyone who can edit requests can ask for someone else's records.
Another common failure is relying on UI filtering instead of real authorization. Hiding another user's messages in the screen does not protect anything if the API already returned them.
The Fix Plan
My fix plan is defensive and staged so I do not make an already-bad situation worse.
1. Contain exposure first.
- Temporarily disable any endpoint or screen that returns private chatbot history if it cannot be safely filtered yet.
- Rotate any suspected exposed secrets immediately.
- If there is a service role key in the mobile app or shared config, remove it now.
2. Move authorization to the server or database layer.
- Every private record must be protected by a rule that checks ownership or tenant membership.
- Do not trust `user_id` from the client unless it is only used as a hint and then verified against authenticated identity.
- For multi-tenant data, enforce both `tenant_id` and `user_id` where appropriate.
3. Tighten database policies to least privilege.
- Private tables should default to deny-all except explicit read/write paths.
- Public content should live in separate tables from private chat transcripts.
- Storage buckets for uploads should require per-user access checks.
4. Remove admin credentials from Expo runtime code.
- Any secret used to bypass security must live only on trusted backend infrastructure.
- If you need privileged operations like moderation or analytics aggregation, move them behind server APIs or edge functions.
5. Fix queries so they only request scoped data.
- Filter by authenticated user ID at query time even if policies already protect it.
- Use pagination and explicit field selection instead of broad `select *`.
- Avoid returning internal metadata unless it is required by the screen.
6. Add hard stops around suspicious access patterns.
- Log denied reads with request IDs but never log raw tokens or full message content.
- Rate limit endpoints that enumerate conversations or attachments.
- Add alerts when one account accesses unusually many records in a short window.
7. Validate before redeploying production traffic back onto it.
- Run two-account tests against staging with production-like policies enabled.
- Confirm old sessions are invalidated after logout if needed.
- Only ship once I have evidence that unauthorized reads now fail closed with 403 or empty results.
A safe rule of thumb: if you are unsure whether a query should return data, make it return nothing until identity is verified.
Regression Tests Before Redeploy
I would not redeploy this fix without a short but strict QA pass. For this kind of issue, correctness matters more than visual polish.
Acceptance criteria:
- User A cannot read User B's chats, prompts, files, profiles, or embeddings through the app or direct API calls.
- Anonymous users cannot access private records at all.
- Service credentials never appear in client-side bundles or logs.
- Logout fully clears local session state on iOS and Android test devices.
Test plan: 1. Two-account isolation test
- Sign in as User A and create 3 chats with messages and attachments.
- Sign out, sign in as User B on the same device, then verify zero access to User A's data.
2. Direct request test
- Replay an authenticated request while swapping identifiers in query params or body payloads.
- Confirm the backend ignores client-supplied ownership claims and uses server-side identity only.
3. Storage access test
- Attempt to open another user's uploaded file URL from within the app flow after sign-in as a different account.
- Confirm access fails unless explicitly shared.
4. Session reset test ```ts await SecureStore.deleteItemAsync("session"); await AsyncStorage.clear(); ``` After logout, relaunch the app and confirm no private screen auto-loads from cache.
5. Negative-path UX test
- Unauthorized screens should show a clean error state rather than broken spinners or blank pages forever.
- The user should see "You do not have access" instead of leaked metadata fragments.
6. Observability check
- Verify denied requests are logged with timestamps and request IDs only.
- Confirm no sensitive message content appears in error logs or crash reports.
I would also require one manual exploratory pass on both iOS and Android because mobile session bugs often hide behind platform-specific storage behavior.
Prevention
The real fix is not just one policy change. It is building guardrails so this does not come back during another fast AI feature sprint.
Security guardrails:
- Default private tables to deny-all policies before adding exceptions.
- Review every new table with an ownership model before launch day.
- Keep admin keys server-only and rotate them quarterly at minimum.
Code review guardrails:
- No merge goes out without checking auth boundaries first: who can read this row, who can write it, who can delete it?
- Reject any PR that uses client-provided IDs as proof of identity without verification at the backend layer。
- Require small changes with clear rollback paths instead of large mixed refactors during incident recovery.
Monitoring guardrails:
- Alert on spikes in cross-user reads, failed auth attempts, and unexpected 200s on protected endpoints.
- Track p95 API latency under 300 ms for normal chatbot fetches so security fixes do not silently break performance later。
- Watch crash-free sessions separately from privacy incidents; both affect trust but need different responses。
UX guardrails:
- Show clear loading states while authentication resolves so users do not see other people's cached content briefly during startup。
- Make logout explicit about clearing local data if chats are device-cached。
- If content cannot be loaded securely yet,fail closed rather than showing partial data。
Performance guardrails:
- Use narrow selects instead of broad payloads to reduce accidental exposure surface area。
- Cache only public assets at Cloudflare edge; never cache personalized chat responses publicly。
- Keep third-party analytics scripts away from sensitive screens unless they are strictly necessary。
When to Use Launch Ready
This sprint fits best when:
- You already have a working React Native + Expo chatbot prototype,
- The issue involves domain setup,email,Cloudflare,SSL,deployment,secrets,or monitoring,
- You need a secure handover instead of another round of patchwork fixes。
What I would want from you before kickoff:
- Repo access plus current deployment platform details,
- Database provider access,
- A list of environments: dev,staging,production,
- Any recent incident notes,screenshots,or user reports,
- Current domain registrar credentials if DNS needs cleanup,
- One person who can approve changes quickly during the 48-hour window。
What you get in Launch Ready:
- DNS,redirects,subdomains,
- Cloudflare setup,
- SSL,
- caching decisions,
- DDoS protection basics,
- SPF/DKIM/DMARC,
- production deployment,
- environment variables,
- secrets handling,
- uptime monitoring,
- handover checklist。
If your product has already leaked customer data once,I would treat launch readiness as part security cleanup,part release engineering。That keeps you from shipping a second incident under deadline pressure。
Delivery Map
References
1. Roadmap.sh Cyber Security Best Practices: https://roadmap.sh/cyber-security 2. Roadmap.sh API Security Best Practices: https://roadmap.sh/api-security-best-practices 3. Supabase Row Level Security docs: https://supabase.com/docs/guides/database/postgres/row-level-security 4. Expo Security docs: https://docs.expo.dev/guides/security/ 5. OWASP Access Control Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Access_Control_Cheat_Sheet.html
---
Take the next step
If this is a problem in your product right now, here is what to do next:
- [Use the free Cyprian tools](/tools) - estimate cost, score app risk, check launch readiness, or pick the right service sprint.
- [Book a discovery call](/contact) - I will tell you honestly whether you need a sprint or if you can DIY the next step.
*Written by Cyprian Tinashe Aarons - senior full-stack and AI engineer helping founders rescue, launch, automate, and scale AI-built products.*
Cyprian Tinashe Aarons — Senior Full Stack & AI Engineer
Cyprian helps founders rescue, secure, deploy, and automate AI-built apps with production-grade engineering, launch systems, and AI integration.