Why Login Deserves Extra Scrutiny
A login form is one of the easiest things to ask AI for, and one of the easiest to get wrong in a way you won't notice until it's a problem. AI can produce a form, a "sign up" button, and a page that says "Welcome back, [name]" — and it will look finished. What's easy to miss is whether passwords are actually stored safely, whether a session can be forged, and whether a "protected" page is protected on the server or just hidden by the interface.
The good news is the same as with payments: you don't need to understand cryptography to do this safely. You need to avoid one common mistake — building your own password storage — and confirm one thing AI tools sometimes skip: that protected pages check the session on the server, not just in the browser.
If an AI tool ever generates code that saves a password directly into your database — even briefly, even for testing — stop. A password should never exist in your database as plain, readable text; it should be run through a one-way hashing function before it's stored, and ideally you shouldn't be writing that code at all. The fix below avoids this entirely by letting a dedicated auth provider handle passwords for you.
The Safe Default: A Hosted Auth Provider
Just as hosted checkout delegates the hardest parts of payment handling, a hosted authentication provider delegates password storage, verification, sessions, and recovery. Depending on the provider, people may use a hosted screen, a pre-built component, or a form in your app that calls the provider's SDK. The important boundary is that your database and backend never store or process the password themselves; the provider receives it and your app works with the resulting session.
- Supabase Auth — a natural fit if you're already using Supabase for your database (see Growing Your Vibe Coded App); pair it with Row Level Security policies that restrict every table to the signed-in user's rows.
- Clerk — offers pre-built sign-up and login components as well as custom flows; your own backend must still decide which records and actions each signed-in user may access.
- Firebase Auth — Google's option and a good match if you're already using Firebase for data storage; pair it with Firebase Security Rules that enforce access by user ID.
All three keep password storage entirely on the provider's side. Writing your own login system with a library like bcrypt is possible and sometimes appropriate for larger apps, but it's much more surface area to get wrong — start with a hosted provider unless you have a specific reason not to.
The Prompt to Ask For
Name the hosted provider explicitly so AI doesn't default to writing its own login form, and ask for protected-route handling in the same prompt so it isn't left as an afterthought:
Add email/password sign-up and login to my app using [Supabase Auth / Clerk / Firebase Auth]. Do not write custom password hashing or storage — use the provider's built-in auth. Include: (1) sign-up and login forms that call the provider's SDK, (2) a way to check the current session on both the client and the server or provider-enforced data layer, (3) at least one protected page that redirects to login if there's no valid session, checked before the page's data loads, and (4) authorization rules that let each user read and change only their own records. List which configuration values are safe in browser code and which are backend secrets. Do not rely on hiding a key to protect data.
When the AI returns code, check for these three things before you consider it done:
- No password ever touches your own database or backend code — it should go straight from the login form to the provider's SDK.
- Protected pages check the session before rendering private data, not after — the next section explains why the order matters.
- Only client-safe configuration is used in browser code — such as a Supabase publishable key, Clerk publishable key, or Firebase web configuration. Admin, secret, and service-role keys stay in backend-only code.
- Every data operation has an ownership or permission rule — being logged in is not enough to let someone read or change another user's records.
Why Hiding a Link Isn't the Same as Protecting Data
It's tempting to "protect" a page by simply not linking to it unless someone is logged in, or by hiding a component with an if (loggedIn) check in the browser. Don't stop there: anyone can type the page's URL directly, and browser-side JavaScript can be inspected or bypassed entirely. Hiding a link changes what a logged-out user sees; it does nothing to stop them from requesting the page or its data directly. And checking only that someone is logged in still isn't enough: the app must also check that this particular user is allowed to access this particular record or action.
A page is actually protected when the system that fetches or returns private data verifies both a valid session and permission to access the requested record. That enforcement may live in your server or backend function, or in a provider-enforced data layer such as Supabase Row Level Security or Firebase Security Rules. The client-side redirect that sends a logged-out visitor back to the login page is useful, but it isn't the security boundary. The real boundary is the rule that runs before data is read, changed, or sent.
In practice: the protected route, API endpoint, or database rule verifies the user's session and compares their user ID or permission with the requested resource before allowing the operation. Test this with two accounts: create data as Account A, then try to read, edit, and delete it as Account B by changing the record ID or calling the data endpoint directly. Account B should be denied every time. A page that merely redirects logged-out users in the browser, with no matching check on the data request itself, isn't protected — it's decorated.
Show me every place that reads or changes data for my protected page. Confirm each operation verifies the session and checks that the current user owns the requested record or has an explicit permission before it reads, updates, or deletes anything. If data is accessed directly from the browser, show the provider-enforced rules that make the same check. Also give me a two-account test that proves one user cannot access another user's data.
Password Reset and Email Verification
These two flows are easy to skip in a first pass, because the app "works" without them — right up until a real user forgets their password or signs up with a typo'd email. Both are standard features in every hosted auth provider; you're wiring them up, not building them from scratch.
- Password reset. A user requests a reset, gets an emailed link with a time-limited token, and sets a new password. Ask AI to use the provider's built-in reset flow rather than inventing a custom "security question" system.
- Email verification. Confirms the email address is real and owned by the signer-upper, catching typos before they lock someone out of their own account. Decide up front whether unverified users can use the app at all, or only after confirming — and make sure the AI-generated code matches your answer.
Add password reset and email verification using [provider]'s built-in flows. Unverified users should [be able to use the app with a banner asking them to verify / be blocked from [specific feature] until verified]. Show me where each email template and redirect URL is configured.
Test Users Before Real Ones
Keep development and production accounts separate so experiments and test users never touch real data. How you do this depends on the provider: Clerk supplies distinct development and production instances, while Supabase and Firebase projects are commonly separated by environment. Each environment gets its own configuration and backend secrets.
- Build and test sign-up, login, password reset, and your protected page against the development project first, using accounts you control.
- Only point your app at the production environment once the full flow — sign up, verify, log in, hit a protected page, fail the two-account access test, and log out — works end to end.
- Keep every backend secret or elevated key out of your repository and AI chats. Some browser configuration is public by design; an environment variable can keep environments tidy, but it does not make a value secret once it is shipped to the browser. Protect data with server-side authorization, Row Level Security, or Firebase Security Rules — not an obscured key. See Sanitizing Code and Data Before Sending to AI if you're unsure what's safe to share.
Pre-Launch Accounts Checklist
- Hosted auth provider, not a custom password system — your app should never store or hash a raw password itself.
- Identity and permission enforced where every data operation runs — on your server or through provider-enforced data rules, not just hidden in the browser.
- Two-account isolation tested — one user cannot read, edit, or delete another user's records, even by changing an ID or calling the endpoint directly.
- Only client-safe configuration in browser code — admin, secret, and service-role keys stay backend-only and are never committed to the repo.
- Password reset and email verification wired up — using the provider's built-in flows, not a custom one.
- Full flow tested against a development project — sign up, verify, log in, protected page, log out — before pointing at production.
- A privacy policy in place if you're collecting emails or other personal data — see the data-handling section of Growing Your Vibe Coded App.
Related Guides
Adding Payments to Your Vibe Coded App
The natural next step once users can log in — hosted checkout, webhooks, and what to check before real money moves.
Growing Your Vibe Coded App
Adding features, handling real users, and keeping an eye on hosting and storage costs as your app grows.
Sanitizing Code and Data Before Sending to AI
What to scrub before pasting code or credentials into an AI conversation — including API keys.