You test login. It works. You test logout. It works. You ship.
Three weeks later a user emails: they reset their password but can still log in with the old one. Someone else is complaining they get booted every time they close the browser. A third person says they created an account, never verified their email, and somehow has full access to features that are supposed to be gated.
None of this is a bug the AI will catch in the next prompt. All of it was predictable. And all of it comes from the same root cause: the AI built you the happy path, and the happy path is not a security model.
Auth is the most-rebuilt, least-tested part of any vibe-coded app. This is the part where you learn what was quietly missing.
Why auth feels working when it isn't
Here's what "auth works" usually means when you've just finished prompting it: you can sign up, log in, and see your dashboard. The button says you're logged out when you click logout. The test account you made behaves correctly.
That's one scenario. Real auth has dozens.
What happens when a user forgets their password and requests a reset? Does the old password stop working immediately after the reset, or does it stay valid until a session expires? What happens if two browser tabs are open, the user logs out in one, and then tries to do something in the other? What if they never clicked the verification email link? Are they locked out, or do they have a working account with an unverified email address sitting on your database?
The AI doesn't know which of those outcomes you want, so it picks defaults. The defaults are usually whatever the library's quickstart example does. That's not malicious. It's just the happy path doing what happy paths do: getting from A to B cleanly without covering the edges.
The problem is that auth edges are not edge cases. They're the scenarios every user who isn't you will eventually hit.
The specific defaults that will bite you
A few patterns come up repeatedly in AI-generated auth code, regardless of whether you're using Lovable, Bolt.new, Cursor, or another tool.
Client-side gating. The AI will often protect a route by checking if a user is logged in on the frontend and redirecting if not. That's fine for the UI. It's not security. A logged-out user who knows the direct URL can still hit the API endpoints that serve the protected data, because those aren't gated. Real protection happens on the server: row-level security policies in your database (rules that say "this user can only read their own rows"), or middleware that verifies the session token before touching any data.
Missing row-level security. Row-level security, or RLS, is a database feature that enforces "user A can only see and modify their own data" at the database level, not the application level. Without it, a user who figures out someone else's user ID can potentially read or overwrite that person's data by crafting a direct API request. Supabase enables RLS per table; it's off by default. The AI doesn't always turn it on unless you ask.
Leaky JWTs. A JWT (JSON Web Token) is a short-lived credential your app hands to the user after login so they don't have to re-authenticate on every request. "Short-lived" is relative. Some AI-generated setups default to tokens that expire after a week, or thirty days, or never. If a token leaks (copied from the browser, grabbed from a log file, or exposed in a request header), it's valid until it expires. If it never expires, you have a permanent backdoor into that account.
No email verification gate. Signing up with fake@whatever.com will often land you directly in the app. The verification email exists, but there's no check that says "if the email isn't verified, block access to the paid features." The AI builds the form and sends the email; the gate is a separate step it frequently skips unless explicitly specified.
Permanent sessions across devices. The "remember me" behaviour your AI sets up might mean the user never gets logged out. Log in on a work computer, forget to log out, leave the job, and the session is still valid months later. There's no way for the user to see active sessions or revoke them.
These aren't obscure edge cases. They're the failure modes that show up in the first month after you ship, and they all come from a spec that didn't define what auth is supposed to do, just that it should exist.
This pattern is the same shape as what happens with billing: the AI builds what's visible and quietly skips what isn't. If you've already read the post on adding Stripe to a vibe-coded app without breaking it, you've seen this before.
Magic links vs password vs OAuth: which one is actually right for you
Before writing a single spec line, you need to decide what login method you want. The AI will generate one by default; it might not be the right one.
Password-based auth is what most people expect. Email and password, reset flow, the whole thing. It's familiar and requires no third-party service. The tradeoff is that users forget passwords constantly, reset flows have a lot of states to handle correctly, and storing passwords (even hashed) adds responsibility.
Magic links are email-only login: the user enters their email, receives a link, clicks it, and they're in. No password to forget, no reset flow. Very good for apps where users aren't logging in daily. The tradeoff is that login depends on email access, which adds a step that can feel slow on mobile.
OAuth (social login, "Sign in with Google") removes the password problem entirely and users convert faster because they're not creating a new account. The tradeoff is you depend on a third party. If Google changes something, your login can break. Some users (indie privacy types, in particular) won't use it on principle.
For most indie apps with a small audience who know you, magic links are underrated. They're simpler to implement correctly than passwords, they have no password-storage liability, and the UX is perfectly fine for an app people use weekly rather than daily. If you're building something where users will open it every single day, password auth makes more sense.
OAuth is worth adding alongside one of the above once you have users, not instead of them. Make it additive.
The spec needs to answer this before you start prompting, because the decision cascades into every other auth detail.
Six lines your auth spec needs
This is the part where most vibe-coded apps skip from "I want login" straight to the AI generating 400 lines of auth code. The six questions below are the ones that determine whether that code is actually safe or just looks safe.
1. Session lifetime. "Sessions expire after 7 days of inactivity. Refresh tokens are valid for 30 days max. After 30 days, the user must log in again." Pick numbers. Don't leave it to defaults.
2. Reset flow behaviour. "After a password reset, all existing sessions are invalidated immediately, including on other devices. The reset link expires after 30 minutes."
3. Email verification timing. "Users can sign up but cannot access paid features until their email is verified. Unverified accounts have access to [list what exactly] only."
4. Role model. "There are two roles: free user and paid user. Roles are stored in the database and checked server-side on every protected API call. The client never decides access level." Even a simple two-role model needs to be explicit, because the default is often one role and a boolean flag the client reads.
5. Row-level security. "RLS policies are enabled on all tables that contain user data. Policies restrict all reads and writes to the authenticated user's own rows by default." Say this explicitly. It's often the difference between "secure by default" and "secure if the frontend is behaving."
6. "Log me out everywhere." "The app exposes a way for users to revoke all active sessions at once." If you don't spec it, it won't exist. Most vibe-coded apps ship with no session management UI at all.
These six lines won't write the auth for you, but they'll prevent the AI from making six silent choices that are hard to reverse later. This is the same principle as the pre-launch checklist for vibe-coded apps: the things that matter most are the ones you have to name explicitly.
The feature almost nobody ships: active session management
Here's one that's missing from nearly every indie app, AI-generated or otherwise.
Your users have no way to see where they're logged in.
A paying user on your app might have an active session from their home Mac, their work PC, their phone, and a tablet they haven't touched in four months. They have no way to know, and no way to log out of all of them without logging in on each device and clicking logout.
This matters for a few reasons. It's a basic privacy feature users will eventually expect. It's useful when someone suspects their account was accessed without permission. And it's a trust signal: apps that show session management look like apps that thought about security.
The implementation isn't complex. A table in your database with one row per active session (device, browser, last active timestamp, a revoke button). The AI can build it if you ask. The problem is that nobody asks, because nobody thinks about it until a user asks where to find it.
Adding "users can view and revoke active sessions" to your auth spec before you start prompting costs you one line and gets you a feature that most apps at three times your scale still don't have.
The spec is what makes auth actually work
The gap in authentication for vibe-coded apps isn't that the AI writes bad code. It's that auth has far more states than a form with a submit button, and the AI can only cover the states you describe.
Write down the session lifetime. Write down what happens on password reset. Write down who has access to what and when. Write down that RLS is required, not optional. Then hand that spec to Cursor, Lovable, Bolt.new, or whichever tool you're using, and the auth that comes back will actually cover the scenarios real users hit.
Draftlytic is built to help you get these decisions out of your head and into a structured spec before you start prompting. You describe the app, answer a few questions about how it should work, and the output is a plan that already has the security decisions named. That's what the AI needs to build the thing that works, not just the thing that looks like it works.
Auth is boring to get right. It's a lot more boring to fix after the fact.