TL;DR: Anyone on the internet can tell what database your product runs. They read it from your DNS and your page source. No code access needed. So database hardening starts from one honest fact: the front door address is public. This checklist closes what stands behind it. Row-level security (RLS) on each table. Keys out of the client. Locked buckets. Rate limits. Tested backups. One alert that tells you when something breaks. Most steps are one SQL statement.

QuestionAnswer
Can people see my database tech?
Yes. DNS records and script paths give it away from outside.
What is the one control that matters most?
RLS on each table. The public key is harmless only when RLS is on.
Where do AI tools go wrong?
New tables ship with RLS off. Each schema change can re-open the door.
How long does hardening take?
One afternoon. Ten steps, most of them one statement or one setting.
How do I verify it worked?
Scan your live URL from the outside, the way an attacker would.

Your database is not hidden. Stop hardening as if it were.

Founders picture their database as a box in a locked room. It is closer to a shop window on a busy street.

Here is what a stranger learns about your product in under a minute. No login. No code access. Our scanner runs these same checks on each URL it audits.

When our scanner confirms a Supabase backend, it adds a warning to the report. The wording is blunt on purpose:

“Supabase detected: verify row-level security policies on all tables. The anon key is public by design — RLS is your only access control layer.”

That line, from platformFingerprintScanner.js:396, is the whole threat model in one sentence. The address is public. The key is public. The one thing that decides who reads your data is the rule layer inside the database itself. Hardening is the work of making that layer real.

The post-launch database hardening checklist

Work top to bottom. The order matters. The first four steps close off your data. The rest keep it closed and make sure you notice if it opens again. Set aside one afternoon for a normal vibe-coded SaaS product, website or app.

1. Find each table with RLS off

This is the most useful query in this guide. Run it in the SQL editor first:

select tablename
from pg_tables
where schemaname = 'public'
  and rowsecurity = false;

Each table it returns is readable with the public key that already sits in your page source. The list should be empty. It almost never is on a first scan. AI tools create tables with RLS off by default, and they never come back to fix it. We wrote up how that happens in Supabase RLS in vibe-coded apps.

2. Turn RLS on and write deny-by-default rules

Turning RLS on takes one statement per table. With no policies, an enabled table denies all reads and writes. That is the correct starting point. Then grant back only what the product needs:

alter table public.orders enable row level security;

create policy "owners read own orders"
on public.orders
for select
using ((select auth.uid()) = user_id);

Write one policy per action. A table that customers only read needs no insert rule at all. Our guide to Supabase RLS policies has patterns for the common shapes: user-owned rows, team access, and public read.

3. Make the rules fast, not just correct

Note the sub-select around auth.uid() above. Without it, Postgres calls the function once for each row it checks. With it, once per query. On a big table that is the gap between a fast page and a timeout. Add an index on each column your rules filter on. The details are in RLS performance.

4. Keep the service role key out of the client

The public key is meant to be seen. The service role key skips every rule you just wrote. Search your repo and your deployed bundle for it. It belongs in server env vars only. It must never ship to a browser. If the two keys blur together, read anon key vs service role key before you go further.

5. Lock down storage buckets

Tables get the attention. Buckets hold the passports, invoices and exports. They have their own rule system, and AI tools skip it just as often. Make each bucket private unless the product truly serves public files. Then write bucket rules with the same deny-by-default habit. The walkthrough is in storage bucket security.

6. Tighten the auth settings you never opened

Confirm email should be on. OTP expiry should be short. The redirect allow-list should hold only your own domains. Each one is a checkbox. Each one is a known hole when left on defaults. The full pass is in our Supabase auth security checklist.

7. Make server-side functions check who is calling

Edge functions often run with full rights. An open function is a side door around all of your rules. Check the caller’s token inside each function that touches data. Patterns are in edge functions auth.

8. Put a rate limit between the internet and the database

Each request your API accepts becomes database work. With no limit, one person with a loop can run your bill up and your service down. Our burst probe at k6Scanner.js:173 finds take-anything APIs all the time. Set a per-IP limit on public endpoints. The rate limiting guide for founders shows the middleware in a few lines.

9. Keep backups you have actually restored

Hardening protects against attackers. Backups protect against you, last Tuesday, running a migration with a typo. Auto backups are table stakes. The step founders skip is the restore test. Once a month, restore into a scratch project and time it. Database backups for vibe-coded apps covers how long to keep them, plus point-in-time restores.

10. Turn on the boring lights: logs and one alert

Watching your database does not mean buying a big console. It means you can answer two questions. What ran against my data today? Will I hear about it when something fails? Keep query logs on, and wire one alert for error spikes. In our scans, this is the weakest of the four areas we test. Most vibe-coded products ship with nothing watching at all. Start with error monitoring basics.

Real finding — P0

Live scan, June 2026: a production Supabase backend was found from DNS alone (platformFingerprintScanner.js:28, confidence 0.99). The public key sat in the client bundle. The API took a full request burst with zero 429 responses (k6Scanner.js:173). Translation: the address, the key, and an unmetered door. All public. Each of the ten steps above closes part of that finding.

On Firebase? Same checklist, different names.

Each step above has a direct twin. Security rules play the role of RLS, and the same open-until-locked default is the same trap. Our Firebase security rules guide maps it out. The habit to keep is the same: deny by default, then grant back on purpose.

Make the check repeatable, or it will drift

Hardening is not a one-time cleanup. Your schema does not stand still. Each prompt that adds a feature can add a table. That table arrives with RLS off. Put the step-1 query somewhere you will re-run it. Add the pre-launch checklist to your release habit. Keep the security glossary open for any term that made you nod without knowing why.

See what the internet sees

The free scan reads your product from the outside: DNS, page source, public endpoints. You get a Launch Readiness Score out of 100 in about thirty seconds. No code access. No sales call.

Run the free scan — $0

FAQ

What is database hardening?

Database hardening is the work of closing each path to your data that your product does not need. That means RLS on each table. Keys kept out of the client. Locked storage buckets. Rate limits in front of the API. Tested backups. And an alert that fires when something odd happens. It is a checklist, not a project. Most steps are one SQL statement or one setting.

Is the Supabase anon key safe to expose?

Yes, by design, but only if row-level security is on. The anon key ships in your page source and anyone can read it. It is safe because it should have no power on its own. All access control must live in RLS rules on the tables. If a single table has RLS off, the public key reads it.

How do I find tables with row-level security turned off?

Run one query: select tablename from pg_tables where schemaname = 'public' and rowsecurity = false. Each table that query returns is open to anyone who holds your public key. The list should be empty. Re-run it after each schema change, because AI tools create new tables with RLS off by default.

Does row-level security slow my database down?

A badly written policy can. The common mistake is calling auth.uid() once per row instead of once per query. Wrap the call in a sub-select, so the planner runs it one time. Then add an index on the column your rule filters on. With those two habits, the extra cost of RLS on a normal SaaS workload is small.

How often should I test my database backups?

Restore one backup into a scratch project at least once a month. A backup you have never restored is a guess, not a plan. Time the restore, write the number down, and check that the restored data passes a simple row-count spot check against production.

How do I check my database security from the outside?

Run a scan against your live URL. Our free scan is $0. It takes about thirty seconds and returns a Launch Readiness Score out of 100. It reads only what an attacker can read: your DNS, your page source, and your public endpoints.

Run the free scan — $0, about 30 seconds