TL;DR: Supabase Auth is the authentication system built into Supabase. It handles email/password login, magic links, and OAuth providers like Google, GitHub, and Discord — all with a few lines of code that AI generates reliably. The secret weapon is its integration with Row Level Security (RLS): database rules that ensure users can only see and edit their own data. Without RLS, auth is just a front door with no locks inside the house. AI often skips RLS because the app "works" without it. It does — until someone reads all your users' private data.
Why AI Coders Need This
Every app eventually needs login. Whether it's a SaaS dashboard, a personal tracker, or a side project you're showing friends — at some point, you need to know who's using it and keep their data separate from everyone else's.
If you're building with AI tools like Claude, Cursor, or Lovable, you've probably already encountered Supabase Auth. It's one of the most common auth systems that AI reaches for, and there's a good reason: the API is clean, the documentation is excellent, and AI models have been trained on thousands of Supabase Auth examples. When you say "add Google login," your AI knows exactly what to generate.
But here's the problem that catches nearly every vibe coder off guard: authentication and authorization are two different things. Authentication proves who you are. Authorization decides what you're allowed to do. Supabase Auth handles authentication beautifully — it knows that you're user #47. But without Row Level Security policies on your database tables, user #47 can query user #48's private notes, user #49's payment history, and every other row in every table.
AI tools frequently generate working auth code that's missing the authorization layer. The app runs, login works, data loads — and the security hole is completely invisible until someone exploits it. This article covers both halves so you ship something that actually works and is safe.
Authentication is the process of proving who someone is — like showing your ID at a bar. If terms like "OAuth," "JWT," or "session token" feel unfamiliar, start with What Is Authentication? and come back here. This article assumes you understand the basic idea of logging in.
Real Scenario: "Add Login to My App"
You've been building a personal finance tracker. It works locally — you can add expenses, see charts, everything's great. Now you want to deploy it so you and your partner can both use it, each seeing only your own expenses.
You open your AI tool and type this:
Prompt to your AI:
"Add Supabase authentication to my expense tracker. I want Google login and email/password signup. Each user should only see their own expenses. Use Row Level Security."
That last sentence — "Use Row Level Security" — is the most important thing in the entire prompt. Without it, your AI will probably generate auth that works but leaves every expense visible to every user. Let's look at what good AI output looks like for this prompt.
What AI Generated
A solid AI response to this prompt generates three things: the Supabase client setup, the auth functions, and the RLS policies. Here's what each piece looks like and what it does.
1. The Supabase Client
// lib/supabase.js
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
This creates the connection to your Supabase project. Two things to notice: the URL points to your specific project, and the anon key is a public key that's safe to expose in frontend code. It's not a secret — it's designed to be in the browser. The anon key only allows access that your RLS policies permit. Without RLS, it allows everything. With RLS, it allows only what you've explicitly defined. That's why RLS matters so much.
Supabase gives you two keys: the anon key (public, goes in frontend) and the service_role key (secret, server-only, bypasses all RLS). If your AI puts the service role key in your frontend code, every user has full admin access to your entire database. See Secrets Management for how to handle these correctly.
2. The Auth Functions
// Sign up with email/password
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password-123'
})
// Sign in with email/password
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'secure-password-123'
})
// Sign in with Google OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'http://localhost:3000/auth/callback'
}
})
// Sign out
const { error } = await supabase.auth.signOut()
// Get the currently logged-in user
const { data: { user } } = await supabase.auth.getUser()
This is the core of Supabase Auth from your frontend's perspective. Each function call does exactly what it says. signUp creates a new account. signInWithPassword logs in an existing user. signInWithOAuth redirects to Google's login page and handles the entire OAuth flow — the token exchange, the user profile fetch, everything. You don't touch any of that. Supabase handles it.
When a user signs in, Supabase creates a session — a JWT (JSON Web Token) stored in the browser. Every request your app makes to Supabase automatically includes this token, which tells Supabase who's making the request. You don't pass it manually. The Supabase client library handles it.
3. The Database Table with RLS
-- Create the expenses table
CREATE TABLE expenses (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users(id) NOT NULL,
description TEXT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
category TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Enable Row Level Security
ALTER TABLE expenses ENABLE ROW LEVEL SECURITY;
-- Policy: Users can only see their own expenses
CREATE POLICY "Users can view own expenses"
ON expenses
FOR SELECT
USING (auth.uid() = user_id);
-- Policy: Users can only insert their own expenses
CREATE POLICY "Users can insert own expenses"
ON expenses
FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Policy: Users can only update their own expenses
CREATE POLICY "Users can update own expenses"
ON expenses
FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Policy: Users can only delete their own expenses
CREATE POLICY "Users can delete own expenses"
ON expenses
FOR DELETE
USING (auth.uid() = user_id);
This is the piece that makes everything secure. Let's break it down.
Understanding Each Part
The user_id Column
Every row in the expenses table has a user_id that points to the user who created it. The REFERENCES auth.users(id) part means this must be a valid user ID from Supabase Auth's internal users table. This is the link between "who's logged in" and "whose data is this."
Enabling RLS
ALTER TABLE expenses ENABLE ROW LEVEL SECURITY; — this single line changes everything. Before this line, the anon key can read every row. After this line, the anon key can read zero rows — unless you create policies that explicitly allow access. RLS is deny-by-default. No policy = no access.
The auth.uid() Function
This is the magic glue. auth.uid() is a PostgreSQL function that Supabase provides. It returns the ID of the currently authenticated user — extracted from the JWT that the browser sends with every request. When your RLS policy says auth.uid() = user_id, it means: "only allow this operation if the logged-in user's ID matches the user_id column on this row." User #47 asks for expenses? They only get rows where user_id matches their ID. User #48's expenses are invisible.
USING vs WITH CHECK
USING controls which existing rows a user can see or act on. WITH CHECK controls what values a user can write. For INSERT, WITH CHECK (auth.uid() = user_id) means you can't insert a row with someone else's user_id — you can only create expenses tagged to yourself. For UPDATE, you need both: USING to find the row, WITH CHECK to validate the new values.
Session Management
When a user logs in — whether via email, Google, or magic link — Supabase creates a session and stores it as a JWT in the browser. This token is automatically refreshed before it expires (the default is 1 hour, with a refresh token that lasts longer). You can listen for auth state changes:
// Listen for login/logout events
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
console.log('User logged in:', session.user.email)
// Redirect to dashboard, fetch user data, etc.
}
if (event === 'SIGNED_OUT') {
console.log('User logged out')
// Redirect to login page
}
if (event === 'TOKEN_REFRESHED') {
// Session was automatically refreshed — you usually don't need to do anything
}
})
This event listener is how your app reacts to auth changes. When a user logs in, you redirect them. When they log out, you clear the UI. When the token refreshes, it happens silently in the background. Your AI will usually set this up in your app's root component or layout.
Auth Providers: Your Login Options
Supabase Auth supports multiple ways for users to log in. Here are the most common ones AI tools set up:
| Provider | How it works | Best for |
|---|---|---|
| Email/Password | Classic signup form. User creates account with email and password. Confirmation email optional. | Any app. The universal fallback. |
| Magic Link | User enters email, receives a login link. No password needed. Clicks link → logged in. | Apps where you want friction-free signup. Great for MVPs. |
| Google OAuth | "Sign in with Google" button. Redirects to Google, user authorizes, redirects back logged in. | Consumer apps. Most users have a Google account. |
| GitHub OAuth | Same flow as Google but with GitHub. Popular in developer tools. | Dev tools, coding platforms, technical audiences. |
| Discord OAuth | Login with Discord account. Common in community and gaming apps. | Community apps, gaming, Discord bot dashboards. |
| Phone/SMS | User enters phone number, receives OTP code via SMS. | Mobile-first apps, two-factor authentication. |
All OAuth providers follow the same pattern in code: supabase.auth.signInWithOAuth({ provider: 'github' }). Swap 'github' for 'discord' or 'google' and the flow is identical. The configuration happens in the Supabase Dashboard — you paste in your OAuth client ID and secret from each provider, and Supabase handles the redirect dance.
What AI Gets Wrong
AI tools are remarkably good at generating Supabase Auth code. The API is well-documented, the patterns are consistent, and there are tons of examples in training data. But there are specific failure modes that show up repeatedly. Here's what to watch for.
1. Skipping RLS Entirely
This is the #1 problem. Your AI generates a beautiful auth flow — signup, login, logout all work perfectly. It creates database tables with a user_id column. Everything seems right. But it never runs ALTER TABLE ... ENABLE ROW LEVEL SECURITY or creates any policies. The app works, tests pass, you deploy — and every authenticated user can query every row in every table.
How to catch it: After AI generates your database schema, search for ENABLE ROW LEVEL SECURITY. If it's not there, tell your AI:
Follow-up prompt:
"Enable Row Level Security on all tables and create policies so users can only read, insert, update, and delete their own rows. Use auth.uid() = user_id for each policy."
2. Using the Service Role Key in Frontend Code
The service role key bypasses all RLS. It's meant for server-side admin operations — like a master key. Sometimes AI puts it in your .env.local with a NEXT_PUBLIC_ prefix, which exposes it to the browser. Anyone who opens DevTools can read it and has full, unrestricted access to your entire database.
How to catch it: Check your environment variables. Any key starting with NEXT_PUBLIC_ or VITE_ is visible in the browser. Only the anon key should be there. The service_role key should only exist in server-side environment variables. See Secrets Management for the full breakdown.
3. Forgetting the Auth Callback Route
OAuth login works by redirecting to Google (or GitHub, etc.), then redirecting back to your app. That "redirect back" URL needs to hit a specific route in your app that exchanges the temporary code for a session. AI sometimes generates signInWithOAuth with a redirectTo URL but forgets to create the actual callback handler at that route.
Symptoms: User clicks "Sign in with Google," gets redirected to Google, authorizes, gets redirected back to your app — and lands on a 404 page or the login screen again (not logged in).
// app/auth/callback/route.js (Next.js App Router example)
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
export async function GET(request) {
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
if (code) {
const supabase = createRouteHandlerClient({ cookies })
await supabase.auth.exchangeCodeForSession(code)
}
return NextResponse.redirect(requestUrl.origin)
}
4. Not Handling Auth State on Page Load
AI often sets up login/logout but forgets to check if the user is already logged in when the page loads. The user logs in, closes the tab, reopens it — and the app shows the login screen even though their session is still valid.
Fix: Check for an existing session when your app initializes:
// On app load
const { data: { session } } = await supabase.auth.getSession()
if (session) {
// User is already logged in — show the dashboard
} else {
// No session — show the login page
}
5. Mixing Up Client Libraries
Supabase has different client packages for different frameworks: @supabase/supabase-js for vanilla JS, @supabase/auth-helpers-nextjs for Next.js, @supabase/ssr for server-side rendering. AI sometimes imports from the wrong package or mixes them, creating session management bugs where the user appears logged in on the client but not on the server.
How to Debug Supabase Auth Issues
Auth bugs are uniquely frustrating because they're often invisible — the app "works" but security is broken, or the user appears logged out when they shouldn't be. Here's a systematic approach.
Check RLS Status First
Go to Supabase Dashboard → Table Editor. Click on each table. In the table header, you'll see either "RLS enabled" or "RLS disabled." If any table with user data has RLS disabled, that's your problem. Enable it and create policies.
You can also check via SQL:
-- Check RLS status for all tables
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
-- rowsecurity = true means RLS is enabled
-- rowsecurity = false means ANYONE can access ALL rows
Test as a Different User
The easiest way to verify data isolation: create two test accounts. Log in as User A, create some data. Log in as User B in an incognito window. Can User B see User A's data? If yes, your RLS policies are missing or broken.
Check the JWT in Browser DevTools
Open DevTools → Application → Local Storage → your Supabase domain. You'll see a key like sb-[project-ref]-auth-token. The value is a JSON object containing the access token. You can decode the JWT at jwt.io to see what user ID and role it carries. If the role is anon but you expect authenticated, the user isn't actually logged in.
Check the Auth Logs
Supabase Dashboard → Authentication → Logs shows every auth event: signups, logins, token refreshes, and errors. If OAuth redirects are failing, the error message is usually here. Common issues include redirect URL mismatches (the callback URL in your code doesn't match what's configured in the Supabase Dashboard) and expired OAuth credentials.
When auth isn't working and you can't figure out why, try this prompt: "My Supabase auth login redirects to Google but the user isn't logged in when it comes back. Show me how to debug this step by step. Check: callback route exists, redirect URLs match in Supabase Dashboard, session is being set correctly, and RLS isn't blocking the user's data."
Common Error Messages and What They Mean
| Error | What it means | Fix |
|---|---|---|
new row violates row-level security policy |
Your INSERT or UPDATE doesn't match any RLS policy | Check your WITH CHECK policy — usually auth.uid() doesn't match the user_id being inserted |
JWT expired |
The access token timed out and didn't auto-refresh | Make sure you're using the Supabase client (it auto-refreshes). If using a custom setup, call supabase.auth.refreshSession() |
Invalid login credentials |
Wrong email or password | Verify the user exists. Check if email confirmation is required but the user hasn't confirmed |
| Empty data, no error | RLS is working — the policy blocks access but doesn't throw an error. It just returns empty results. | Check that the user is actually authenticated and that the user_id values in the table match auth.uid() |
redirect_uri_mismatch |
The OAuth callback URL doesn't match what's configured in Google/GitHub | Match the exact URL (including http vs https and trailing slash) in the OAuth provider dashboard and Supabase Dashboard |
What's Next
Once Supabase Auth and RLS are working, your app has a solid foundation. Here's where to go from here:
- Add more OAuth providers — Each provider is a 5-minute setup in the Supabase Dashboard. GitHub and Discord are the next most common after Google.
- Build a user profile table — Supabase Auth stores basic info (email, name from OAuth). For anything extra (avatar, preferences, subscription status), create a
profilestable linked toauth.users. - Set up email templates — Customize the confirmation, magic link, and password reset emails in the Supabase Dashboard.
- Add role-based access — When you need admins, editors, and viewers with different permissions, store roles in a
user_rolestable and reference them in your RLS policies. - Understand the bigger picture — Read Supabase vs Firebase to understand how Supabase Auth compares to Firebase Auth. If you're evaluating database options, Supabase vs Neon covers the PostgreSQL hosting landscape.
- What Is Authentication? — The fundamentals of proving who a user is
- What Is OAuth? — How "Sign in with Google" actually works under the hood
- Supabase vs Firebase — Which backend should your AI use?
- Supabase vs Neon — Comparing managed PostgreSQL platforms
- Secrets Management — Keep your API keys out of your frontend code
Frequently Asked Questions
Is Supabase Auth free?
Supabase Auth is free for up to 50,000 monthly active users on the free tier. That covers email/password, magic links, and all OAuth providers. You only hit paid territory ($25/month Pro plan) if you need advanced features like custom SMTP, phone auth beyond free limits, or if your project exceeds the free tier's overall limits. For most side projects and MVPs, it's genuinely free.
Can I use Supabase Auth without the Supabase database?
Technically yes — Supabase Auth is built on GoTrue, an open-source auth server, and you can self-host just the auth component. But you'd lose the biggest benefit: the tight integration between auth and Row Level Security on your database. The whole power of Supabase Auth is that auth.uid() works directly inside your database policies. If you only want auth, consider a dedicated provider like Clerk or Auth0. If you want auth plus a database, Supabase Auth shines because they're designed as one system.
What's the difference between Supabase Auth and Firebase Auth?
Both handle login, OAuth, and session management. The key difference is how they connect to your database. Supabase Auth integrates directly with PostgreSQL through Row Level Security — your auth policies live inside the database as SQL. Firebase Auth works with Firestore Security Rules, written in a custom rules language. Supabase is open source and self-hostable; Firebase Auth is proprietary. Both are mature and reliable. For a full comparison, see Supabase vs Firebase.
Why does AI sometimes skip Row Level Security when setting up Supabase?
AI tools optimize for "make it work." A table with RLS disabled technically works — queries return data, inserts succeed, everything looks fine in development. RLS adds complexity (policies, auth.uid() checks), and AI often takes the shorter path to a working demo. The problem is invisible: there are no error messages, no warnings, no broken UI. The app simply has no data isolation. Always explicitly mention RLS in your prompt when asking AI to set up Supabase tables.
How do I add Google login to my Supabase app?
Three steps: (1) Create OAuth credentials in the Google Cloud Console — you need a Client ID and Client Secret. (2) In the Supabase Dashboard, go to Authentication → Providers → Google, enable it, and paste your credentials. (3) In your frontend code, call supabase.auth.signInWithOAuth({ provider: 'google' }). That's it. Supabase handles the redirect flow, token exchange, and session creation. The user clicks "Sign in with Google," authorizes, and lands back in your app logged in.