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.

