How to Add Authentication to an AI-Built App (Past the Login Form)

The short version: A login form is roughly 10% of authentication - the real work is authorization (row-level security, roles, sessions, and reset flows) that AI builders skip because nothing in your prompt forced them to think about it.
You asked your AI builder for "user login," and it gave you a login form. Email field, password field, a Supabase or Clerk call wired in, and a redirect to a dashboard after sign-in. It works. You can sign up, sign in, sign out. From the outside it looks done.
Then a second user signs up, and you notice something. Open a second browser in incognito, create user B, and load the dashboard. User B can see user A's data. The orders, the projects, the messages - all of it. The login form authenticated the user. Nothing stopped them from reading everyone else's rows.
This is the wall. A login form is roughly 10% of authentication. The other 90% is authorization, and it is the part AI builders consistently skip because nothing in your prompt forced them to think about it. Here is what real auth requires, what the AI gave you by default, and how to close the gap.
| Layer | What the AI builder gives you | What is actually missing |
|---|---|---|
| Login form | Email and password fields, an SDK call, a redirect | Nothing - this part works |
| Roles and RBAC | A single "logged in" state | Admin, owner, member, viewer roles gated on the server |
| Row-level security | Tables with RLS off by default | Policies so user B cannot read user A's rows |
| Sessions and tokens | A token after sign-in | httpOnly cookies, expiry, server-side sign-out |
| Password reset and email verification | A happy-path sign-up | Expiring single-use reset links, blocked unconfirmed users |
| Social-login redirects | A "Sign in with Google" button | Production callback URLs, not localhost |
Authentication Is Not Authorization
These two words get collapsed into "auth," and that collapse is exactly where AI-built apps break.
Authentication answers "who are you." It is the login form: verify a password, issue a session token, identify the user. AI builders are good at this part because it is a well-worn pattern with an SDK call at the center of it.
Authorization answers "what are you allowed to do." Can this user read this row. Can they edit this record. Can they hit this admin endpoint. Should they see this project that belongs to a different account. Authorization is per-resource, per-role, and specific to your data model - which means there is no generic SDK call that does it for you. Someone has to decide the rules and enforce them on every read and write.
Supabase's own documentation draws this line explicitly in its Auth overview: authentication verifies identity with a JWT, and authorization is enforced separately through row-level security policies on the database. The AI gave you the first half. The second half is a stack of decisions it never made.
The Incognito Test: Find the Hole in 60 Seconds
Before any of the fixes, confirm you actually have the problem. Most AI-built apps do.
- Sign up as user A. Create some data - a project, an order, a note.
- Open an incognito or private window. Sign up as user B with a different email.
- As user B, load the same pages: the dashboard, the list view, the detail pages.
If user B sees user A's data, your app has no authorization layer. The login worked; the access control does not exist. You can often shortcut this further: open your browser's network tab as user B, watch the API or database query the page fires, and check whether it filters by the current user's ID at all. If the query is select * from orders with no where user_id = ..., every signed-in user is reading the whole table.
This is not a corner case. A February 2026 audit by VibeEval scanned 1,645 apps built with Lovable and found more than 170 - roughly 10% - had completely exposed databases with no row-level security enabled, leaking home addresses, financial data, API keys, and payment records (Lovable security report, Feb 2026). The login forms on those apps all worked fine. We wrote about the broader pattern in vibe coding security risks.
Row-Level Security: The Layer the AI Skipped
The fix for the incognito test is row-level security, or RLS. Instead of trusting your application code to remember a where user_id = ... clause on every single query, you push the rule down into the database itself. The database refuses to return rows the current user is not allowed to see, no matter what query the app sends.
Here is the trap: in Postgres - and therefore Supabase - tables created through the SQL editor or table editor have RLS off by default. The AI builder creates your orders table, RLS is disabled, and every authenticated request can read and write the entire table. The login form distracted you from the fact that the database door was never locked.
A minimal policy looks like this:
alter table orders enable row level security;
create policy "users read their own orders"
on orders for select
using (auth.uid() = user_id);
The first line turns the lock on. The second says: a user can only select rows where the user_id column matches their authenticated ID. You repeat this for insert, update, and delete, and for every table that holds user data. Supabase's row-level security guide covers the policy syntax, the auth.uid() helper, and the performance patterns that matter once you have real traffic. Supabase also ships a project-level "Enable RLS on new tables" toggle - turn it on so the next table the AI creates is not born wide open.
If you want the deeper mental model of how the JWT, the database, and the policies fit together, the Supabase auth overview lays out the full picture.
Roles and RBAC: When "Logged In" Is Not Enough
The incognito test catches the worst hole. The next layer is roles.
Most real apps have more than one kind of user. An admin who can see every account. A team owner who manages members. A read-only viewer. A regular member. "Is this person logged in" is a yes/no question; "is this person an admin" is a different question, and your AI builder almost certainly did not generate a role system because you did not ask for one in those words.
Role-based access control, or RBAC, means you attach a role to each user and gate actions on that role. The admin dashboard checks for role = 'admin' before it renders. The "delete workspace" endpoint checks for role = 'owner' before it runs. The critical detail: these checks must live on the server or in the database, not in the frontend. Hiding the admin button in React stops nobody - the endpoint behind it is one fetch call away. Auth0's RBAC documentation describes the model of roles-to-permissions cleanly, and the same shape applies whether you use Auth0, Clerk, or roles stored in your own database and enforced through RLS policies.
If you are on Clerk, route protection belongs in middleware so an unauthorized user never reaches the page handler at all. Clerk's clerkMiddleware reference shows how to protect routes by auth state and by role before the request hits your code.
Sessions, Tokens, and the Reset Flow Nobody Tested
Past roles, a cluster of edge cases decides whether your auth holds up in production. AI builders generate the happy path and stop.
Session and token handling. When a user signs in, they get a token. Where does it live - an httpOnly cookie, or localStorage where any injected script can read it. Does it expire. Does signing out actually invalidate the session server-side, or does it just clear the client and leave a valid token in play. The default the AI picks is rarely the secure one.
Password reset and email verification. The reset flow is a maze of edge cases: the reset link has to expire, it has to be single-use, and it must not let an attacker reset an account they do not own. Email verification has its own: what happens to a user who signs up but never confirms - can they still log in and use the app. AI builders frequently leave the "unconfirmed user" gap wide open. Supabase's password-based auth guide walks through the confirmation and reset flows and the configuration that closes those gaps.
Social login redirects. "Sign in with Google" looks trivial until you deploy. The OAuth redirect URL is hardcoded to localhost, or the production callback URL was never added to the provider's allowlist, and the whole flow dead-ends with a redirect error the moment a real user tries it. Supabase's social login guide documents the redirect URL configuration that has to match between your provider and your deployed domain.
What "Done" Actually Looks Like
Run this checklist against your app. It is the difference between a login form and authentication you can put real users behind.
- The incognito test passes: user B cannot see user A's data, on any page or API call.
- RLS is enabled on every table holding user data, with policies for select, insert, update, and delete.
- Roles exist if your product has more than one kind of user, and role checks run on the server or in the database - never only in the UI.
- Sessions live in httpOnly cookies, expire, and invalidate on sign-out.
- Password reset links expire and are single-use; unconfirmed users cannot access protected data.
- Social login redirect URLs are configured for your production domain, not localhost.
None of this is exotic. It is the standard, unglamorous work of access control - the 90% the demo never shows. The reason it stalls AI-built apps is not difficulty; it is that the model optimizes for the visible result, and a working login form looks like the finish line when it is barely the start.
When the Gap Is Bigger Than a Weekend
If your app already has users, fixing this is delicate. You cannot just flip RLS on across a live database and hope nothing breaks - a policy that is slightly too strict locks legitimate users out, and one that is slightly too loose leaves the hole open. Multi-tenant apps, team hierarchies, and role systems that have to coexist with data you have already collected are exactly where the 30-40% that AI builders skip turns into a real engineering project.
This is the same wall on the payments side, where a working checkout button hides the unsexy correctness work underneath - we cover that in the companion piece on adding Stripe payments to an AI-built app. When the auth layer is the thing standing between you and launch, and retrofitting it cleanly is past a weekend's work, that is the kind of finish-the-hard-part problem Creatr was built to take off your plate - shipping the access control, the policies, and the role system as production code rather than a prompt you keep re-rolling.
The login form was never the hard part. Now you know where the hard part actually lives.
Common questions
- Why can one user see another user's data in my AI-built app?
- Your app authenticates users with a login form but never enforces authorization. The database has no row-level security, so every signed-in user can read the whole table. The fix is enabling RLS policies that filter rows by the authenticated user's ID on every read and write.
- What is the difference between authentication and authorization?
- Authentication answers who you are - the login form that verifies a password and issues a session. Authorization answers what you are allowed to do - whether you can read a specific row, edit a record, or hit an admin endpoint. AI builders handle the first and skip the second, which is where access control actually lives.
- How do I test whether my app's authorization is broken?
- Run the incognito test. Sign up as user A and create data. Open a private window, sign up as user B, and load the same pages. If user B sees user A's data, you have no authorization layer. Check the network tab too: if queries lack a user_id filter, every user reads the full table.
- Is Supabase RLS enabled by default on new tables?
- No. Tables created through the SQL editor or table editor have row-level security off by default, so every authenticated request can read and write the entire table. You must enable RLS per table and write policies for select, insert, update, and delete. Supabase offers a project-level toggle to enable it on new tables.

Co-founder and CEO of Creatr. Spends his time with founders who have tried every AI coding tool and still can't ship. Before Creatr, Kartik was a serial founder; the last of those startups found product-market fit in early 2020 and was ultimately shut down by the COVID standstill. Covered by Forbes India in 2021.
Related reading
- Vibe Coding Security Risks: What Founders Need to Know Before Going LiveAI tools produce apps that look correct and ship with serious security gaps. Here are the six failure modes that appear in vibe-coded apps - and what to check before real users depend on yours.
- What Happens When Your Vibe-Coded App Actually Gets UsersThe app is live, real people are using it, and something is going wrong that was never visible in development. Three specific failure modes that hit vibe-coded apps when they get real users - and what each one looks like from the outside.
- What to Do When Your No-Code App Hits Its LimitThe app works. You have users. Revenue is coming in. And something is wrong in a way that is becoming impossible to ignore. The four types of no-code ceiling and the three paths forward.