fixes / launch-ready

How I Would Fix database rules leaking customer data in a Cursor-built Next.js mobile app Using Launch Ready.

The symptom is usually simple and scary: one customer can see another customer's records, or a support agent spots private rows in the mobile app that...

How I Would Fix database rules leaking customer data in a Cursor-built Next.js mobile app Using Launch Ready

The symptom is usually simple and scary: one customer can see another customer's records, or a support agent spots private rows in the mobile app that should never have loaded. In a Cursor-built Next.js app, the most likely root cause is bad authorization at the data layer, not just a buggy UI.

The first thing I would inspect is the actual request path from the app to the database. I want to know whether the app is reading directly from a client-exposed database SDK, whether row-level rules are missing or too broad, and whether any server route is accidentally returning unfiltered data.

Triage in the First Hour

1. Check the last 10 production requests for the affected endpoint.

  • Look at response bodies, status codes, user IDs, and tenant IDs.
  • Confirm whether the leak is cross-user, cross-tenant, or only visible to signed-out users.

2. Inspect auth logs and session claims.

  • Verify that every request has a valid user ID and tenant scope.
  • Look for missing JWT claims, expired tokens, or sessions that are being accepted without proper checks.

3. Review the database rule set immediately.

  • Check row-level security policies, table grants, service-role usage, and any public read permissions.
  • Confirm whether policies exist for `SELECT`, `INSERT`, `UPDATE`, and `DELETE`, not just one of them.

4. Open the Next.js data-fetching code.

  • Inspect `app/`, `pages/`, server actions, API routes, and any client-side calls in Cursor-generated code.
  • Search for direct queries that bypass your intended API layer.

5. Check deployment environment variables.

  • Confirm production secrets are not exposed to the browser bundle.
  • Verify that staging keys are not being used in production.

6. Review recent builds and merges.

  • Find the commit where data access changed.
  • Look for generated code that added a broad query like "fetch all rows" or "select *" without tenant filters.

7. Inspect monitoring and error logs.

  • Check for spikes in 200 responses with unusually large payloads.
  • Look for warnings around unauthorized reads or missing policy errors that were suppressed by fallback logic.

8. Reproduce safely with two test accounts.

  • Use two different customer identities with separate tenant records.
  • Confirm whether account A can ever see account B's rows through normal UI navigation.
## Quick checks I would run during triage
grep -R "service_role\|select \*\|from('customers')\|from(\"customers\")" .
grep -R "cookies()\|headers()\|getServerSession\|auth" app pages lib

Root Causes

1. Missing row-level security rules

  • Confirmation: I query as a normal user and can read rows without any tenant filter.
  • What it means: The table is effectively public or only protected by weak application logic.

2. Overbroad policy logic

  • Confirmation: The policy uses conditions like `true`, `auth.uid() IS NOT NULL`, or checks only organization membership loosely.
  • What it means: Any authenticated user can read more than they should.

3. Service role key used in client-side code

  • Confirmation: The browser bundle contains privileged database access or an API route proxies everything with admin credentials.
  • What it means: The app bypasses user-level authorization entirely.

4. Server route missing tenant enforcement

  • Confirmation: The API accepts an ID from the client and returns whatever record matches it without verifying ownership.
  • What it means: A valid user can enumerate IDs and pull other customers' data.

5. Generated Cursor code copied insecure patterns

  • Confirmation: Recent diffs show fast-generated queries with no auth guardrails or schema checks.
  • What it means: The build optimized for speed over access control correctness.

6. Stale caches or CDN layers serving private data

  • Confirmation: A response meant for one user appears in another session after refresh or logout/login on shared devices.
  • What it means: Private responses are being cached without proper cache keys or no-store headers.

The Fix Plan

I would fix this in layers, starting at the database and then tightening the app. The goal is to stop exposure first, then make sure future changes cannot reopen the hole.

1. Freeze risky writes and reads if needed

  • If customer data is actively leaking, I would temporarily disable the affected endpoint or switch it to maintenance mode for non-admin users.
  • That is better than letting more private records escape while we patch blindly.

2. Remove any privileged database access from the browser

  • No service-role credentials in client code, ever.
  • Any admin-level access must live only on server routes or background jobs with strict authorization checks.

3. Add strict row-level security

  • Every customer-facing table should enforce ownership or tenant membership at the database layer.
  • Policies should be explicit per operation and should default to deny unless allowed.

4. Enforce authorization again on the server

  • Even with RLS, I still verify user identity in Next.js server actions and API routes.
  • The server should reject requests where `user_id` does not match ownership or tenant membership before querying sensitive tables.

5. Narrow every query

  • Replace broad reads with scoped queries using authenticated identifiers from trusted session context only.
  • Never trust IDs sent directly from mobile clients unless they are checked against session claims.

6. Lock down caching behavior

  • Mark private responses as non-cacheable unless they are safely keyed per user and tenant.
  • If Cloudflare or Next.js caching is involved, I would verify private endpoints use correct cache-control headers.

7. Audit secrets and environment variables

  • Move all production secrets into secure environment storage only accessible to server runtime.
  • Rotate any key that may have been exposed through logs, build output, or client bundles.

8. Add observability before redeploying

  • Log authorization failures separately from generic errors so we can see abuse patterns without leaking data into logs.
  • Track unexpected response sizes, cross-tenant access attempts, and repeated 403s on sensitive routes.

9. Patch one path at a time

  • I would not refactor authentication, schema design, caching, and UI all at once.
  • Small safe changes reduce launch risk and make rollback possible if something breaks.

A clean version of what I expect at minimum looks like this:

// Server-side check before reading private customer data
const session = await getSession()

if (!session?.user?.id) {
  return new Response("Unauthorized", { status: 401 })
}

const customer = await db.customer.findFirst({
  where: {
    id: params.customerId,
    ownerId: session.user.id,
  },
})

if (!customer) {
  return new Response("Not found", { status: 404 })
}

Regression Tests Before Redeploy

I would not ship this fix until these tests pass on staging with production-like data shape but fake records.

1. Cross-account access test

  • Account A cannot read Account B's records through UI navigation, direct URL entry, API calls, or refreshes.
  • Acceptance criteria: 0 successful unauthorized reads across 20 attempts.

2. Tenant isolation test

  • Users only see records tied to their own organization or workspace.
  • Acceptance criteria: every list endpoint returns only scoped rows for that session context.

3. Signed-out access test

  • Anonymous users cannot hit private endpoints successfully.
  • Acceptance criteria: all protected routes return 401 or redirect appropriately.

4. Cache isolation test ``` curl https://staging.example.com/api/customers/123 \ --header "Cookie: session=USER_A"

Then repeat as USER_B against a different account context.
- Acceptance criteria: no shared cached payloads between sessions; private responses must vary correctly by user context.

5. Mobile flow sanity check
   - Open login, list view, detail view, edit flow, logout flow on iOS-sized viewport width.
   - Acceptance criteria: no stale data appears after logout/login switching accounts on the same device.

6. Negative permission test suite
   ```bash
npm run test -- --grep "authorization|tenant|private-data"
  • Acceptance criteria: tests cover unauthorized reads, forbidden writes, missing claims, expired sessions, and malformed IDs.

7. Error handling review

  • Confirm unauthorized users see a safe empty state or permission message instead of internal details.
  • Acceptance criteria: no stack traces, query text, table names, or secret values appear in UI errors.

8. Performance sanity check

  • Make sure adding auth checks did not blow up latency beyond reason.
  • Acceptance criteria: p95 on sensitive endpoints stays under 300 ms on staging for normal load; no new N+1 query pattern appears.

Prevention

I would put guardrails around this so Cursor-generated speed does not create another leak later.

  • Database rules first:

Every sensitive table gets deny-by-default policies reviewed before merge.

  • Code review checklist:

I check authz paths before style changes. If a PR touches data access without ownership checks, it does not ship.

  • Security scanning:

Run dependency audits and secret scans on every deploy branch so leaked keys do not sit unnoticed in repo history.

  • Observability:

Alert on unusual row counts returned per request, repeated forbidden attempts, and admin-key usage outside expected jobs.

  • UX protection:

Private screens should show clear loading states plus safe empty states so developers do not add insecure fallback queries just to avoid blank screens.

  • Test coverage target:

I want at least 80 percent coverage around auth-sensitive services and route handlers before launch work continues elsewhere.

  • Deployment hygiene:

Separate staging from production credentials completely. One accidental config swap can expose real customer records during testing.

  • AI-assisted development guardrail:

When using Cursor or similar tools, I treat generated code as untrusted until I verify auth boundaries manually line by line because AI will happily produce working but unsafe shortcuts if asked vaguely enough.

When to Use Launch Ready

Use Launch Ready when you need this fixed fast without turning it into a month-long rewrite.

I would ask you to prepare:

  • Production and staging repo access.
  • Database admin access plus read-only logs if available.
  • Cloudflare account access if DNS or caching is involved.
  • Your current auth flow description with screenshots of broken screens.
  • A list of tables that store customer data and which ones are multi-tenant versus per-user.
  • Any recent error reports showing who saw what they should not have seen.

My approach is simple: 1) stop exposure, 2) prove isolation, 3) redeploy safely, 4) monitor for recurrence over the next 24 hours।

References

  • https://roadmap.sh/api-security-best-practices
  • https://roadmap.sh/code-review-best-practices
  • https://roadmap.sh/cyber-security
  • https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
  • https://supabase.com/docs/guides/database/postgres/row-level-security

---

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.*

Next steps
About the author

Cyprian Tinashe AaronsSenior Full Stack & AI Engineer

Cyprian helps founders rescue, secure, deploy, and automate AI-built apps with production-grade engineering, launch systems, and AI integration.