Supabase Authentication: Implement OAuth, Magic Links and Row-Level Security
Authentication is the part of building apps that nobody wants to deal with. You'd rather be shipping features, not debugging why OAuth tokens expire at 3 AM.
That's exactly why Supabase Auth exists. It handles the painful stuff—OAuth providers, passwordless magic links, session management—so you can focus on what actually makes your app unique.
Let me show you how to set it up properly.
What You're Getting
Supabase Auth isn't just another auth library. It's a complete authentication system built on PostgreSQL, which means your auth data lives right next to your application data. No external services, no vendor lock-in, no surprise pricing.
Here's what we'll cover:
- OAuth with Google and GitHub (the providers 90% of apps need)
- Magic link authentication (passwordless, because passwords are terrible)
- Row-Level Security policies (the secret weapon most developers ignore)
Prerequisites
You'll need a Supabase project. If you're self-hosting on Elest.io, you're already set. Otherwise, grab your project URL and anon key from your dashboard.
npm install @supabase/supabase-js
Setting Up OAuth Providers
The part everyone messes up? Configuring OAuth redirect URLs. Let's do it right.
First, create your Supabase client:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY
)
Now, here's how you trigger Google OAuth:
async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
queryParams: {
access_type: 'offline',
prompt: 'consent',
}
}
})
if (error) console.error('OAuth error:', error.message)
}
GitHub works identically—just swap 'google' for 'github'. The key insight here: always set redirectTo explicitly. Don't rely on defaults, or you'll spend hours debugging redirect mismatches.
In your Supabase dashboard: Add your redirect URL under Authentication > URL Configuration. Format: https://yourapp.com/auth/callback
Magic Links (The Better Way)
Passwords are a security liability. Magic links eliminate them entirely. User enters email, clicks a link, they're in.
async function sendMagicLink(email) {
const { data, error } = await supabase.auth.signInWithOtp({
email: email,
options: {
emailRedirectTo: `${window.location.origin}/dashboard`,
}
})
if (error) {
console.error('Failed to send magic link:', error.message)
return false
}
return true
}
That's it. Supabase sends the email, handles the token verification, creates the session. You just redirect them after login.
Pro tip: Customize your email template in Authentication > Email Templates. The default looks... generic.
Handling the Auth Callback
Both OAuth and magic links redirect back to your app. Here's how to handle that:
// pages/auth/callback.js (Next.js example)
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import { supabase } from '@/lib/supabase'
export default function AuthCallback() {
const router = useRouter()
useEffect(() => {
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN' && session) {
router.push('/dashboard')
}
})
}, [router])
return <div>Authenticating...</div>
}
Row-Level Security: Your Data's Bodyguard
Here's where Supabase gets interesting. Row-Level Security (RLS) enforces access rules at the database level. Even if your API has bugs, users can only access their own data.
Enable RLS on your table:
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
Now create policies. This one lets users read only their own posts:
CREATE POLICY "Users read own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
And this allows users to insert posts with their own user_id:
CREATE POLICY "Users insert own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
The magic function is auth.uid()—it returns the current authenticated user's ID directly from the JWT. No application code needed.
Here's a complete example for a typical posts table:
-- Full RLS setup for a posts table
CREATE POLICY "Public posts are visible to everyone"
ON posts FOR SELECT
USING (is_public = true);
CREATE POLICY "Users can view their own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users can update their own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "Users can delete their own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);
Session Management
Supabase handles sessions automatically, but you'll want to check auth state in your app:
// Get current session
const { data: { session } } = await supabase.auth.getSession()
// Listen for auth changes
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_OUT') {
// Clear local state, redirect to login
}
})
// Sign out
await supabase.auth.signOut()
Common Gotchas
Redirect URL mismatch: Your app's redirect URL must exactly match what's configured in Supabase. Trailing slashes matter.
RLS blocking everything: If queries return empty after enabling RLS, you probably forgot to create SELECT policies. RLS denies by default.
Token refresh: Supabase auto-refreshes tokens, but if you're storing the session server-side, call getSession() to get fresh tokens.
Deploy on Elest.io
Want to self-host Supabase with full control over your data? Deploy Supabase on Elest.io in minutes. You get automated backups, SSL, and none of the managed service markup.
That's authentication done right. OAuth for convenience, magic links for security, RLS for bulletproof data access. Ship it and move on to the features that actually matter.
Thanks for reading. See you in the next one.