Skip to content

Am I Storing Passwords or API Keys in My Database in Plain Text?

6 min read

Here is the one-line test: if you can open your database, look at a user row, and read their actual password, it is stored wrong. A correctly built app stores a scrambled hash of the password that even you cannot reverse, and it keeps API keys and other secrets out of user-facing tables entirely. If either of those isn't true, fix it before your first real users sign up.

Why this happens

An AI agent builds exactly the feature you describe and nothing you didn't. "Let users sign up with an email and password" is a clear instruction, and the quickest thing that satisfies it is a users table with an email column and a password column, saving whatever the person typed. It works in the demo. The login checks that the stored value matches the typed value, everyone gets in, and nobody mentions that the passwords are sitting there in readable text.

The same shortcut shows up with secrets. If your app talks to Stripe, OpenAI, or a mail provider, the agent may drop that API key into a config table, a settings row, or a column next to user data, because that was the fastest place to put it. None of this is malice. It is the agent solving the sentence you gave it.

How to check

You are looking for two different problems.

  1. Read a password back. Open your database (Supabase Table Editor, Firebase console, or a SQL client) and look at the table that holds users. If you see a column with a real-looking password like hunter2, that is plain text and it is broken. A hashed password looks like random gibberish, often starting with $2b$ (bcrypt) or $argon2. If it's readable, that is the bug.

  2. Search for secrets in your data. Look through your tables for anything starting with sk_live, sk_test, AKIA (AWS), or long random keys. Also check whether any secret lives in your client-side code by searching your built bundle:

    grep -rE "sk_live|sk_test|AKIA|SECRET" ./dist ./build
    

    Any hit in the browser bundle is a live leak, not just a storage question.

The fix

  1. Stop storing passwords yourself. Use the platform's auth. Supabase Auth, Firebase Auth, Clerk, and Auth0 hash passwords for you with a vetted algorithm and never hand them back. If you're on one of these, delete any password column you created and let auth own it. This is the right answer for almost every vibe-coded app.

  2. If you truly must hash yourself, use bcrypt or argon2. Never MD5, SHA-1, or "encryption" you can reverse. Hash on sign-up, compare hashes on login, and store only the hash. There is no legitimate reason to ever read a password back.

  3. Move secrets to server-only environment variables. API keys belong in your host's env settings (Vercel, Netlify, Supabase Edge Function secrets), read only by server code, never in a database row and never in a NEXT_PUBLIC_ variable. See what the anon key is and is it safe to expose for the public-versus-secret distinction.

  4. Rotate anything that was exposed. If a secret ever sat in your database or your bundle, treat it as compromised. Generate a new key in the provider's dashboard and revoke the old one. Storing it correctly now does not un-expose the old value.

The trap to avoid

Do not "encrypt" passwords with a reversible cipher and call it done. Encryption you hold the key to means you (and anyone who steals your database plus that key) can read every password. Passwords must be hashed, which is one-way, not encrypted. The tell is simple: if your login code can turn the stored value back into the original password to compare it, it is reversible and wrong. Compare hashes, never decrypt.

Where this fits

Plain-text passwords and stray API keys are the kind of thing that never breaks the demo and quietly sinks you the moment a real database gets copied, leaked, or poked at. The free Readiness Report runs this exact check against your real project and tells you whether your passwords are hashed and whether any secret is sitting somewhere it shouldn't. If you'd rather have the fixes handed to you in order, that's what the Finishing Pass is for. It pairs well with locking down who can read your tables at all, covered in can anyone read my Supabase database from the browser.