Supabase Auth Architecture
Understanding GoTrue
Supabase Auth is powered by GoTrue, an open-source authentication server originally created by Netlify. Supabase has forked and significantly extended it to integrate tightly with PostgreSQL.
How GoTrue Works
GoTrue is a standalone authentication service that:
- Handles user registration and login
- Stores user data in PostgreSQL
- Issues JWT tokens for authenticated users
- Manages sessions and token refresh
- Integrates with OAuth providers
βββββββββββββββ βββββββββββββββ
β β 1. Login request β β
β Client β βββββββββββββββββββββββββββ GoTrue β
β β β Server β
β β 2. Validate credentials β β
β β β β β
β β β βΌ β
β β β βββββββββββ β
β β β βauth. β β
β β β βusers β β
β β β βββββββββββ β
β β 3. Return JWT β β
β β βββββββββββββββββββββββββββ β
βββββββββββββββ βββββββββββββββ
The auth Schema
GoTrue stores all authentication data in the auth schema:
auth.users
The primary user table:
-- Simplified structure
CREATE TABLE auth.users (
id uuid PRIMARY KEY,
email text,
encrypted_password text,
email_confirmed_at timestamptz,
phone text,
phone_confirmed_at timestamptz,
confirmation_token text,
recovery_token text,
raw_app_meta_data jsonb,
raw_user_meta_data jsonb,
created_at timestamptz,
updated_at timestamptz,
last_sign_in_at timestamptz,
role text, -- 'authenticated' or custom
...
);
auth.identities
Links users to OAuth providers:
-- When user signs in with Google, GitHub, etc.
CREATE TABLE auth.identities (
id text,
user_id uuid REFERENCES auth.users(id),
provider text, -- 'google', 'github', etc.
identity_data jsonb,
created_at timestamptz,
updated_at timestamptz,
...
);
auth.sessions
Active user sessions:
CREATE TABLE auth.sessions (
id uuid PRIMARY KEY,
user_id uuid REFERENCES auth.users(id),
created_at timestamptz,
updated_at timestamptz,
factor_id uuid, -- For MFA
...
);
auth.refresh_tokens
Manages token refresh:
CREATE TABLE auth.refresh_tokens (
id bigint PRIMARY KEY,
token text,
user_id uuid REFERENCES auth.users(id),
revoked boolean,
created_at timestamptz,
updated_at timestamptz,
...
);
Authentication Flow
Email/Password Registration
1. User submits email + password
β
βΌ
2. GoTrue validates input
- Email format valid?
- Password meets requirements?
β
βΌ
3. Create user in auth.users
- Hash password (bcrypt)
- Generate confirmation token
β
βΌ
4. Send confirmation email (optional)
β
βΌ
5. Return user object (no JWT yet if email confirmation required)
Email/Password Login
1. User submits email + password
β
βΌ
2. GoTrue finds user by email
β
βΌ
3. Verify password hash matches
β
βΌ
4. Check account status
- Email confirmed?
- Account not banned?
β
βΌ
5. Create session in auth.sessions
β
βΌ
6. Generate JWT tokens
- Access token (short-lived)
- Refresh token (long-lived)
β
βΌ
7. Return tokens to client
OAuth Flow
1. User clicks "Sign in with Google"
β
βΌ
2. Redirect to Google's OAuth consent
β
βΌ
3. User approves access
β
βΌ
4. Google redirects back with auth code
β
βΌ
5. GoTrue exchanges code for Google tokens
β
βΌ
6. GoTrue fetches user info from Google
β
βΌ
7. Create/update user in auth.users
β
βΌ
8. Create identity link in auth.identities
β
βΌ
9. Generate Supabase JWT
β
βΌ
10. Return to your app with tokens
User Metadata
GoTrue stores two types of metadata:
app_metadata
Controlled by the server/admin, not editable by users:
{
"provider": "email",
"providers": ["email", "google"],
"role": "admin" // Custom roles
}
Access via auth.jwt()->>'app_metadata' in policies.
user_metadata
Editable by the user themselves:
{
"full_name": "Alice Smith",
"avatar_url": "https://...",
"preferences": {
"theme": "dark"
}
}
// Update user metadata
const { data, error } = await supabase.auth.updateUser({
data: { full_name: 'Alice Johnson' }
})
The auth.uid() Function
Supabase provides helper functions for RLS policies:
auth.uid()
Returns the current user's UUID:
-- Use in RLS policies
CREATE POLICY "Users see own data"
ON profiles FOR SELECT
USING (user_id = auth.uid());
How it works:
- Client sends JWT in request header
- PostgREST validates JWT
- Sets PostgreSQL session variable from JWT claims
auth.uid()reads from session variable
auth.jwt()
Returns the full JWT payload as JSON:
-- Access any JWT claim
CREATE POLICY "Admins only"
ON admin_table FOR ALL
USING (
auth.jwt()->>'role' = 'admin'
);
-- Check app_metadata
CREATE POLICY "Premium users"
ON premium_content FOR SELECT
USING (
(auth.jwt()->'app_metadata'->>'subscription')::text = 'premium'
);
auth.role()
Returns the current role:
-- 'anon' for unauthenticated, 'authenticated' for logged in
CREATE POLICY "Authenticated users only"
ON private_table FOR SELECT
USING (auth.role() = 'authenticated');
Session Management
Access Tokens
- Short-lived (default: 1 hour)
- Contain user claims
- Sent with every API request
- Not stored server-side (stateless)
Refresh Tokens
- Long-lived (default: 1 week)
- Used to get new access tokens
- Stored in auth.refresh_tokens
- Can be revoked
Token Refresh Flow
βββββββββββ βββββββββββ
β Client β 1. Access token expired β Server β
β β β β
β β 2. Send refresh token β β
β β ββββββββββββββββββββββββββββββ β
β β β β
β β 3. Validate refresh token β β
β β β β
β β 4. Issue new tokens β β
β β ββββββββββββββββββββββββββββ β β
βββββββββββ βββββββββββ
The Supabase client handles this automatically:
// Automatic token refresh
const supabase = createClient(url, anonKey, {
auth: {
autoRefreshToken: true, // Default
persistSession: true, // Default
}
})
Integration with PostgreSQL
What makes Supabase Auth special is its deep PostgreSQL integration:
1. User Data in PostgreSQL
Unlike services that store users separately, auth data is in your database:
-- Query users (if needed)
SELECT id, email, created_at
FROM auth.users
WHERE email_confirmed_at IS NOT NULL;
2. Foreign Keys to auth.users
Your tables can reference authenticated users:
CREATE TABLE profiles (
id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
-- ...
);
CREATE TABLE posts (
id uuid PRIMARY KEY,
user_id uuid REFERENCES auth.users(id),
-- ...
);
3. Triggers on auth.users
React to authentication events:
CREATE FUNCTION handle_new_user()
RETURNS trigger AS $$
BEGIN
INSERT INTO public.profiles (id, email)
VALUES (NEW.id, NEW.email);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION handle_new_user();
4. RLS Uses auth Functions
Row Level Security policies use authentication context:
CREATE POLICY "Users see own data"
ON profiles FOR SELECT
USING (id = auth.uid());
Configuration Options
Password Requirements
Configure minimum password strength:
- Minimum length
- Required characters (numbers, symbols)
- Password strength checking
Session Settings
- Access token expiry
- Refresh token expiry
- Single session vs. multiple sessions
Email Settings
- Confirmation required
- Custom email templates
- SMTP configuration
OAuth Providers
- Enable/disable providers
- Configure client IDs and secrets
- Redirect URLs
Key Takeaways
- GoTrue is the engine: Handles all authentication logic
- PostgreSQL stores everything: User data, sessions, tokens
- auth.uid() bridges auth and data: Use in RLS policies
- Metadata has two types: app_metadata (server) vs user_metadata (user)
- Automatic token refresh: SDK handles complexity
- Deep integration: Foreign keys, triggers, RLS all work together
Next Steps
Understanding the architecture helps you make better decisions about:
- When to use custom claims
- How to structure user-related data
- Where to put authorization logic
Next, we'll explore the various authentication methods Supabase supports.
GoTrue isn't just an auth serviceβit's designed to make PostgreSQL authentication-aware. This deep integration is what enables Row Level Security to work so elegantly.
Discussion
Sign in to join the discussion.

