Home / Learn / Launch Readiness / Stage 1: Lock down your data

Stage 1 of 1118 min readUpdated 2026-10-09

Stage 1: Lock down your data

Who can read your database? Turn on Row Level Security, keep secret keys out of browser code, test as a stranger, and know your breach-notice clocks.

Facts checked on 2026-10-09. Not legal advice; see the note at the bottom.

The short answer

Most data leaks in apps built with AI tools are not clever attacks. The database answers anyone who asks, because no rule says who may read which rows, or a secret key ships inside the code that runs in the browser. Before launch, turn on access rules for every table, keep secret keys on the server, and test your app as a stranger and as the wrong user. Then write a one-page plan for what you will do if data leaks, because the legal clocks start when you find out.

Why this matters

These are real cases from 2025 and 2026. Each one has a source you can open.

  • Lovable apps with open databases. In March 2025, Matt Palmer scanned 1,645 projects from Lovable's public showcase. He reports that 170 of them had missing or weak database access rules, with 303 vulnerable endpoints, and he only scanned public homepages (Palmer's statement). The issue became CVE-2025-48757, rated 9.3 out of 10. Lovable disputes the record. Its position, quoted in the record, is that each customer "accepts a responsibility over protecting the data of their application" (NVD record). Whatever you think of that, it tells you who the platform thinks is responsible: you.
  • Moltbook. In February 2026, the security firm Wiz found a Supabase key in the browser code of Moltbook, a social network for AI agents whose founder said he did not write the code himself. The key alone was not the problem. The database had no working access rules, so the key gave full read and write access. Wiz reports 1.5 million API tokens, 35,000 email addresses and private messages were exposed. It was fixed within hours of the report (Wiz).
  • Base44. In July 2025, Wiz found that anyone could create an account on a private Base44 app if they knew its app ID, which is not a secret. Wix, which owns Base44, fixed it in under 24 hours and said it saw no sign of abuse (Wiz). This was a platform bug, not a founder mistake. The lesson still applies to you: a login screen does not decide what a user may see, and an ID in a URL is not a password.
  • Tea. In July 2025, about 72,000 images leaked from the Tea dating-safety app, about 13,000 of them selfies and photo IDs from account checks (404 Media). Be careful with this one. Many posts blamed AI-written code, but Tea said the data sat in a legacy storage system with data from before February 2024, and Simon Willison argues AI coding was not to blame (Willison). It is here as an example of storage left open, not as an AI story.
  • Scans of AI-built apps. Escape, a company that sells security tools, scanned over 5,600 public apps built on platforms like Lovable and Base44. It reports more than 2,000 vulnerabilities, more than 400 exposed secrets and 175 cases of exposed personal data, including medical records (Escape).
  • Secrets in code. GitGuardian counted 28.65 million new secrets, such as API keys and passwords, added to public GitHub commits in 2025. It measured a 3.2% secret-leak rate in commits made with Claude Code's help, against 1.5% across all public commits, and it warns this is not a simple tool failure (GitGuardian).
  • Access control is the top web risk. The OWASP Top 10 is the standard list of web app security risks. In its current 2025 edition, Broken Access Control is number one, and OWASP found some form of it in every app it tested (OWASP A01:2025).

None of this means AI tools write unsafe code on purpose. It means they make it fast to ship a database before anyone has decided who may read it.

Core concepts

Authentication is not authorization

Authentication answers "who are you?". It is your login screen. Authorization answers "what may you do?". It is the rule that decides which records this person may read or change. OWASP defines access control as the thing that makes sure "users cannot act outside of their intended permissions" (OWASP A01:2025).

An app can have perfect login and no authorization at all. AI tools often build exactly that, because "only logged-in users" sounds safe.

Worked example. Your notes app has a login. Alice signs in and her browser asks for note 41, which is hers. She changes the request to note 42, which belongs to Bob. If the server or database only checks that Alice is logged in, it sends Bob's note. The fix is a rule that also checks ownership: "send note 42 only if its owner is the person asking."

Firebase shows the same trap in its own docs. A rule that only checks auth != null gets this label: "Not recommended: Any logged-in user has read and write access to your entire database" (Firebase). OWASP's baseline is deny by default: the app must always make a decision to permit or deny, and anything not allowed is denied (OWASP Authorization Cheat Sheet).

Row Level Security, in plain words

Many AI-built apps use Supabase, which is a hosted Postgres database that your app can call straight from the browser. That design is only safe because of Row Level Security (RLS), which Supabase describes as "granular authorization rules that run inside the database" (Supabase RLS guide).

Three facts decide whether your data is safe:

  1. Postgres creates tables with RLS turned off. You must turn it on for each table (Postgres docs).
  2. With RLS on and no policy, nobody can read the table. Postgres calls this a "default-deny policy". A policy is the rule that lets some rows through.
  3. Supabase's own guide is blunt: "A table in an exposed schema without RLS is readable and writable by any role with a grant on it. Enable RLS on every table in an exposed schema."

Worked example. A notes table has a user_id column. These two statements turn RLS on and let each signed-in user read only their own notes:

alter table public.notes enable row level security;

create policy "Users read their own notes"
on public.notes for select
to authenticated
using (auth.uid() = user_id);

That covers reading. Insert, update and delete each need their own policy, or they stay denied. If your agent writes one policy that says using (true), that policy lets everyone through. Read every policy it writes.

Firebase uses Security Rules instead of RLS, with the same idea. When you create a Firebase database you pick Locked mode, which denies everyone, or Test mode, which allows everyone. Test mode is not for production (Firebase). Firebase also says clients "aren't responsible for enforcing security": the rules are (Firebase Security Rules).

The Supabase change on 2026-10-30

There is a second gate in front of each Supabase table, called a grant. A grant lets a database role reach the table at all. RLS then decides which rows that role sees. Supabase calls the automatic access to your tables the Data API.

Before this change, every new table in the public schema got grants for the anon, authenticated and service_role roles, so it was reachable the moment it was made. Supabase is changing that (Supabase changelog):

DateWhat changed
2026-04-28New projects can opt in to explicit grants
2026-05-30Explicit grants become the default for new projects, rolled out over time
2026-10-30Explicit grants apply to all existing projects

After the change, a new table needs an explicit grant before the Data API can reach it. Tables you already have keep the grants they have. RLS works the same as before. Supabase's stated reason is the one this course exists for: "agents, CLI scripts, and AI platforms create tables too, and many of those operations do not have a human reviewing the diff."

Two things to take from this:

  • A grant does not make a table safe. A granted table with RLS off is still open to anyone.
  • Old grants do not go away when you add policies. Supabase says: "On projects that still grant anon and authenticated by default, revoke those grants. Adding policies doesn't remove them" (Supabase RLS guide).

Worked example. After 2026-10-30 your agent adds a comments table and the app shows an error, because the API cannot reach the table. The fast fix an agent may suggest is to grant everything to anon, the role for visitors who are not logged in. Do not accept that fix as it stands. Grant only what the app needs, and turn on RLS with a policy first:

alter table public.comments enable row level security;
grant select, insert on table public.comments to authenticated;
revoke all on table public.comments from anon;

Which keys may live in the browser

Anything your app sends to the browser, every visitor can read. So a key in browser code is a public key, whatever its name says.

Supabase gives you two kinds of key (Supabase API keys):

  • The publishable key (it starts with sb_publishable_) is "Safe to expose online". The older version is called the anon key, which Supabase says is "being deprecated by the end of 2026".
  • The secret key (it starts with sb_secret_) skips RLS completely. The older version is the service_role key. It belongs on your server only, never in a browser, a shipped app or your git history.

The publishable key is only safe because RLS is on. Wiz made the same point about Moltbook: with RLS in place, the public key would have been safe to expose.

Your framework decides what ends up in the browser through a prefix on environment variables, which are the settings your app reads at build time. In Next.js, a variable that starts with NEXT_PUBLIC_ "will be inlined into any JavaScript sent to the browser" (Next.js docs). In Vite, "Variables prefixed with VITE_ will be exposed in client-side source code after Vite bundling" (Vite docs). Think of the prefix as a promise to publish the value.

Worked example. Your .env file has two lines:

NEXT_PUBLIC_SUPABASE_KEY=sb_publishable_...
SUPABASE_SECRET_KEY=sb_secret_...

The first ships to every visitor. That is fine, as long as RLS is on. The second stays on the server, and only server code may use it. If the second line ever gets a NEXT_PUBLIC_ prefix, every visitor gets a key that skips all your rules. After you build the app, search the output for secret key prefixes before you deploy:

grep -rE "sb_secret_|service_role" .next/static dist build 2>/dev/null

Add the prefix of any other secret key you use to the pattern. No output is what you want. A hit means a secret is in browser code.

On GitHub, push protection blocks a push that contains a known secret format. It is on by default for your own pushes to public repositories, and secret scanning runs free on public repositories (GitHub push protection, GitHub secret scanning). If a secret does reach git, rotate it first, which means replace it with a new one at the provider, then clean up. OWASP's guide says to remove exposed secrets at once, logs included (OWASP Secrets Management Cheat Sheet).

The clocks that start if data leaks

A data breach is when personal data leaks to someone who should not have it. Every US state has a law that says you must tell the people affected (NCSL). Many of those laws set a deadline.

The Privacy Rights Clearinghouse reviewed the state laws as of 2026-01-01. It counts 20 states with a fixed number of days, from 30 to 60, and 30 days in California, Colorado, Florida, New York and Washington. Most other states say "without unreasonable delay" (Privacy Rights Clearinghouse).

WhoDeadlineSource
California residents30 calendar days from discovery, under SB 446Bill digest (mirror)
California Attorney GeneralWithin 15 days of telling consumers, if over 500 Californians are affectedBill digest (mirror)
Washington residentsNo more than 30 calendar days after discoveryRCW 19.255.010
EU data authorityWithout undue delay, and where feasible within 72 hours, unless the breach is unlikely to cause riskGDPR Art. 33
EU usersWithout undue delay, when the breach is likely to cause a high risk to themGDPR Art. 34

California's 30-day rule is new. Older guides say "the most expedient time possible", which is out of date. Law-firm summaries report it took effect on 2026-01-01; I read the bill digest, not the final statute text. The GDPR rows link a copy of the text, because the official EU site would not load for my research; the official version is on EUR-Lex.

California also lets people sue a business when their unencrypted personal data is stolen because the business did not keep "reasonable security". After the 2025 inflation update, damages are $107 to $799 per consumer per incident, or actual damages if higher (California Privacy Protection Agency). That right comes from the CCPA, which only covers businesses that pass a size test, such as revenue above a threshold, or buying, selling or sharing the personal information of 100,000 or more California residents or households (California Attorney General). Many solo founders fall below it. The state breach-notice laws are separate laws, so do not assume the CCPA test exempts you from them. Check the law for each state where you have users.

Worked example. On a Monday you learn that a table of user emails was readable by anyone for two weeks. You have users in California and in the EU. Here are your clocks: if the leak could put people at risk, the EU data authority within 72 hours where feasible; California users within 30 calendar days; and if over 500 Californians are affected, the Attorney General within 15 days after you tell them. That is why the plan has to exist before launch. You will not want to research this on the day it happens.

Basic hardening

Two cheap steps close common gaps. Security headers are short instructions your server sends with every page, telling the browser to refuse some easy attacks. The OWASP Secure Headers Project lists them, including Strict-Transport-Security and Content-Security-Policy (OWASP Secure Headers). Rate limiting caps how many tries one visitor gets, which slows password guessing. OWASP recommends throttling login attempts (OWASP Authentication Cheat Sheet), and Supabase Auth already rate-limits its login endpoints per IP address, with limits you can tune (Supabase rate limits).

Key terms

The checklist

The prompt for your agent

Copy this into Claude Code, Codex or Cursor from the root of your project. It asks for evidence, not reassurance.

Audit this project for Stage 1 of a launch checklist: data access and secrets.
Do not change any code, config, migration, or database setting until I approve a plan.

For each item below, report PASS, FAIL, or UNKNOWN, with the file and line (or the
table and policy name) that proves it. If you cannot prove an item from the code,
report UNKNOWN. Never report PASS without evidence.

1. List every database table. For each: is Row Level Security (or a Firebase rule)
   enabled? Which policies exist for select, insert, update, and delete?
2. Flag any policy or rule that allows everyone (for example "using (true)") or that
   only checks for a login ("auth != null") without checking who owns the row.
3. Flag any table that is granted to anon or authenticated but has RLS off.
4. Find every place a database or API key is used. Is any secret, service_role, or
   sb_secret_ key reachable from browser or app code?
5. List every environment variable with a NEXT_PUBLIC_ or VITE_ prefix (or your
   framework's public prefix). Say whether any of them holds a secret.
6. Find every API route or server function that reads data by an ID from the request.
   Does it check that the caller owns that record?
7. Is there rate limiting on login, sign-up, password reset, and one-time codes?
8. Which security headers does the app send (Content-Security-Policy,
   Strict-Transport-Security, X-Content-Type-Options, Referrer-Policy)?
9. Is there a written plan for a data breach anywhere in the repo?

End with a table: item, status, evidence (file and line), and the smallest fix you
suggest. Then stop and wait for my approval.

When the report comes back, check two or three PASS lines yourself. Open the file and line it names. If the evidence does not say what the agent claims, treat the whole report as unproven and ask again.

Common mistakes

  • Turning on RLS and writing no policies, then "fixing" the broken app with a policy that allows everyone.
  • Treating "only logged-in users" as security. Any stranger can make an account.
  • Putting a secret key in a NEXT_PUBLIC_ or VITE_ variable because the app "needed it in the browser". If the browser needs it, it is not secret.
  • Adding RLS policies on an older Supabase project and assuming that removed the old grants. It did not.
  • Deleting a leaked key from the code but not rotating it. The old key still works for anyone who copied it.
  • Trusting a green scan from your platform or your agent without opening the evidence.
  • Waiting for a breach to find out which deadlines apply.

Check yourself

1. Your app has a login screen and every database request needs a signed-in user. What is still missing?

2. You turn on Row Level Security for a Supabase table and write no policies. What happens to API requests from the app?

3. Which of these keys is safe to put in browser code?

4. After 2026-10-30, your agent adds a table on an existing Supabase project and the app cannot reach it. What is the right fix?

5. A secret key was committed to your public repo last week. What do you do first?

6. You find a leak affecting 600 people in California. Under SB 446, what are the deadlines?

Resources

Next stage

Your own database is locked. Stage 2 looks at everything you did not write yourself: the scripts, fonts and services that see your users' data, the contracts you sign with them, and the open-source packages your agent added.

Worked through the checklist and passed the quiz? Mark the stage done. Your progress stays in this browser only.

Talk to Claude instead of typing to it.

Ducktate turns what you say into clean text, on your Mac, and pastes it anywhere.

Get early access