glimfly
guide updated

How do I add login to my vibe coded app?

The short answer

Adding login means wiring up a managed authentication provider, not writing password code yourself. Reach for Supabase Auth, Clerk, or Auth0. All three have free tiers that carry a launch: Supabase and Clerk each cover 50,000 users, Auth0 covers 25,000, as of July 2026. The one part you never build by hand is password storage, because getting salted hashing, session tokens, and reset flows right is a specialist job where a single slip leaks every account at once. “Login” is two systems, not one. Authentication is proving who a user is; authorization is deciding what that user may touch. Beginners build the first, ship, and skip the second, which is how a working sign-in screen ends up sitting on a database any logged-in stranger can read. If your app was built on Lovable it runs on Supabase, so authorization there means turning on row level security for every table and testing it with a second account. Keep your secret key on the server, and check your work as a stranger before anyone else does.

Login is two systems wearing one word

The word “login” hides a split that matters more than anything else on this page. Authentication answers “who are you,” and a provider handles it when a user types a password or taps a Google button. Authorization answers “what are you allowed to see and change,” and that part is yours to build no matter which provider you pick.

The gap bites in a specific way. When a user signs in, the provider hands their browser a signed session token, usually a JWT, that every later request carries to prove the user is still them. That token says who the user is. It says nothing about which rows in your database they’re allowed to read. If you never write the rules for that second question, then the moment anyone logs in with a valid token, your app happily serves them everyone’s data, because it was only ever confirming that they were signed in, not that the thing they asked for was theirs.

Supabase puts it plainly in its own docs: authentication is “checking that a user is who they say they are,” authorization is “checking what resources a user is allowed to access.” A login form gives you the first. You build the second on purpose.

Use a managed provider, and hand-rolling passwords is the one part you skip

If you take one instruction from this guide, take this: do not write your own password storage. It looks like a small table with an email column and a password column, and it is a trap. Doing it correctly means hashing every password with a slow, salted algorithm built for the job, comparing them in constant time so attackers can’t guess by measuring how fast your server answers, issuing and expiring session tokens safely, and building reset flows that can’t be hijacked. OWASP’s Authentication Cheat Sheet, the reference the whole industry leans on, steers you toward the proven functions your framework already ships rather than custom code, and it warns that home-grown comparisons leak secrets through timing. One mistake anywhere in that pile and every account is exposed at once, not one at a time.

Managed providers exist to do exactly this part so you don’t touch it. The three worth knowing:

ProviderFree tier (as of July 2026)Best fit
Supabase Auth50,000 monthly active usersAlready there if you’re on Lovable; auth and database in one place
Clerk50,000 monthly retained usersPolished drop-in sign-in and user-profile UI
Auth025,000 monthly active usersMature, most configuration knobs

One number to read carefully: Clerk bills “monthly retained users,” and a person counts only once they come back at least 24 hours after signing up, so a curious visitor who never returns is free. Auth0 and Supabase count monthly active users, which includes those one-time signups. The practical takeaway is the same, though. For a new app, all three cost nothing, and each one hands you the dangerous, boring cryptography as a solved problem.

The Supabase path, since your Lovable app already runs on it

Lovable projects sit on Supabase, so this is the path most readers are actually on. Supabase Auth handles the sign-in and hands each user a JWT; the authorization half lives in your database through row level security, and that is the step agents skip.

Row level security is the database deciding, row by row, who may read or change what. Supabase is blunt that it “must always be enabled on any tables stored in an exposed schema,” which by default means every table in your public schema. Skip it and the publishable key that ships in every visitor’s browser can read any table it reaches.

Now the trap built specifically for vibe coders. Supabase turns row security on automatically for tables you make in the dashboard’s Table Editor, but not for tables created in raw SQL, and raw SQL is exactly how your agent creates tables when it writes a migration. So an app built end to end by an agent can ship with the rules off on the very tables it generated, even though you never chose that.

Turning security on is half the job. The other half is writing policies. A policy is the actual rule, for example “a user may read a row only when its user_id matches their own id.” Until you write one, enabling row security locks the table completely: Supabase notes that once it’s on, “no data will be accessible via the API when using a publishable key, until you create policies.” So if a screen in your app shows nothing right after you flip the switch, that’s the safe failure, not a bug. Ask your agent to write a policy per table that ties each row to the signed-in user through auth.uid(), then confirm the screen fills back in.

Social login is mostly one setting people get wrong: the redirect URL

“Sign in with Google” runs on OAuth, the standard that lets a user prove their identity to your app through Google without ever handing you their Google password. Your provider does the heavy lifting. Your job is two settings and a habit of exact matching.

First, the redirect URL, the address Google sends the user back to after they approve. It has to match character for character, and a trailing slash or an http where you needed https is enough to break it with a redirect_uri_mismatch error. On Supabase the callback is https://<your-project-ref>.supabase.co/auth/v1/callback, and that exact string goes into the “Authorized redirect URIs” box on your Google OAuth client. Supabase’s own guide adds a step people forget: when you go live, remove the localhost entries you added for testing.

Second, the consent screen, the Google-branded page that asks the user to approve. Configuring it raises a fair question: does Google have to review your app first? For plain login, usually no. If you request only the basic scopes (openid, email, profile), Google treats them as non-sensitive and approves them when you publish, with no demo video and no security review. You still need a homepage and a privacy policy on a verified domain, and while your app sits in “Testing” status it’s capped at 100 users until you publish it. Google’s full verification, the weeks-long process with a demo video and a possible security assessment, only kicks in when you ask for sensitive data like someone’s Gmail or Calendar, which basic login doesn’t.

The agent mistakes that leave the door unlocked

Agent-built auth tends to fail quietly, in the same handful of ways. Walk this list before you ship.

The client-only check. Your agent hides the admin button unless the user is an admin, then calls it done. But anyone can open the browser console or send a direct request to your API and skip your interface entirely. Every rule that actually protects data has to run on the server or in a database policy, never only in the browser.

Row security off, or on with no policies. Covered above, and it’s the most common hole in Supabase apps. Both halves have to be true: security enabled, and a policy written for every table.

The secret key in the browser. Supabase gives you two kinds of key. The publishable key (sb_publishable_..., the old name was the anon key) is built to ship in client code. The secret key (sb_secret_..., formerly the service_role key) is different in kind: Supabase warns that secret keys “bypass Row Level Security and have full access to your data,” so they belong only “on backends you control, out of source control, and out of client code.” An agent that wires the secret key into your frontend to make an error disappear has just handed every visitor your whole database. Keep secrets in environment variables on the server, and if one ever reached client code or a public repo, treat it as an exposed key and rotate it right away.

Over-powered access. When you do need server-side data access, scope the key and the policies to the minimum the feature needs. That’s least privilege: a leak of a read-only, single-table key is an annoyance, while a leak of a key that can do anything is the end of your week.

The test: a stranger, a second account, and where your keys live

Three tests catch nearly every login mistake, and none of them needs an engineer.

The logged-out stranger. Open your live app in a private window, signed out, and try to reach a page or a piece of data that should require login by typing its URL directly. If the data loads, your check was client-only and the server never enforced it.

The second account. Make two accounts, save a note in the first, then sign in as the second and try to read the first account’s note, both through the app and by asking for its row directly. If account two can see account one’s data, your authorization is missing, the row rules are off, or a policy is too loose. This is the single most revealing test you can run, because it’s exactly what a curious real user will try.

The key placement. Search your codebase for your secret key’s prefix (sb_secret_ on Supabase) and for words like secret and service_role. Anything that grants full access should live in a server-side environment variable, never in a file that ships to the browser. While you’re there, adding login is one item on the wider pre-ship checklist worth running in full before launch.

What Glim would tell you

Glimfly is building Glim, a firefly that sits beside your coding agent and explains, in plain language, what it just changed. Auth is the exact moment that habit pays off, because the dangerous mistakes here are silent: a table shipped with its rules off, a secret key quietly moved into the browser to clear an error, a login screen that authenticates everyone and authorizes no one. None of those throw a red error. They just wait. So before you ship login, run the three tests above, and when a term moves faster than your agent explains it, every word here has a plain-language entry in the dictionary. If you’d like Glim watching the terminal with you the next time you add auth, the waitlist is open.

People also ask

“Can I just build my own login with an email and password field?”

You can, but password storage is the one part to hand to a provider. Doing it right means salted, slow password hashing, safe session tokens, reset flows that can't be hijacked, and constant-time comparisons, and a single mistake exposes every account at once. Managed providers (Supabase Auth, Clerk, Auth0) do exactly this part for you, and all three are free at the start: Supabase and Clerk cover 50,000 users, Auth0 covers 25,000, as of July 2026. Wire up their SDK and skip writing password code entirely.

“My login works when I test it, so why is my data still exposed?”

Because login and data access are two separate systems. Signing in proves who a user is, but it does nothing to stop that logged-in user from reading other people's rows unless you also set rules for what each account is allowed to touch. On Supabase that means turning on row level security for every table, writing a policy per table, and testing with a second account to confirm user A can't see user B's data. A working login screen with row rules off is an app where any signed-in visitor can read the whole database.

“Do I need Google to verify my app to add 'Sign in with Google'?”

For plain sign-in, usually not. If you request only the basic scopes (openid, email, profile), Google treats them as non-sensitive and approves them when you publish, with no demo video and no security review. You still need a homepage and a privacy policy on a verified domain, and while your app is in 'Testing' status it's capped at 100 users until you publish it. Full verification only applies if you ask for sensitive data like someone's Gmail or Calendar, which basic login never does.

Words from this guide

security

What's the difference between authentication and authorization?

Authentication verifies who a user is, usually at login, while authorization checks what that verified user is allowed to do or see, and building only the first one is why any logged-in user can end up reading data that isn't theirs.

security

What is a JWT? (the wristband your app checks at the door)

A JWT is a signed token that proves who a user is, so your server can trust it on every request without looking anything up in a database.

security

What is OAuth? (how Sign in with Google actually works)

OAuth is a protocol that lets you grant one app limited access to your account on another service (Google, GitHub, etc.) without ever handing that app your password.

databases

What is RLS (Row Level Security)? The setting that keeps your Supabase data private

Row Level Security is the Postgres rule set that decides, row by row, who can read or write data, and in Supabase it's the only thing standing between your public anon key and your entire database.

security

What is the principle of least privilege? (why your agent keeps asking permission)

The principle of least privilege means every key and account gets the minimum access it needs to do its job, so one leak or one bad command can't take everything down.

security

Your API key is exposed: what happens, what to do, how to prevent it

An exposed API key is a secret that leaked somewhere public, and from that moment it works for whoever finds it, not just you.

files

What is an environment variable? (and why your app has a .env file)

An environment variable is a named value your app reads from its surroundings at runtime, instead of having it hardcoded, so secrets and settings can change without touching a line of code.

free tool · no signup

See this in your own project Glim reads it in plain words.

Paste what your agent just did, a git diff, your terminal, or its summary, and Glim tells you what changed and what to check before you ship. Nothing stored.

Explain my session →