# How to Log Auth Errors in Supabase

- Tool: Supabase
- Difficulty: Intermediate
- Time required: 15-20 min
- Compatibility: Supabase (all plans), @supabase/supabase-js v2+
- Last updated: March 2026

## TL;DR

To log auth errors in Supabase, capture error events from the client-side auth methods (signIn, signUp, signOut) and the onAuthStateChange listener, then store them in a custom auth_error_logs table. For server-side logging, create a database trigger on the auth.users table or use an Edge Function that monitors auth events. This gives you visibility into failed logins, expired tokens, and signup failures for debugging and security monitoring.

## Capturing and Storing Auth Error Events in Supabase

Supabase Auth errors like failed logins, expired tokens, and signup failures are returned to the client but not logged anywhere by default. Without persistent logging, these errors disappear when the user closes their browser. This tutorial shows you how to build an auth error logging system that captures errors from both client-side and server-side, stores them in a database table, and lets you query patterns for debugging and security monitoring.

## Before you start

- A Supabase project with authentication configured
- The Supabase JS client installed in your frontend
- Access to the Supabase Dashboard SQL Editor
- Basic understanding of Supabase Auth methods and RLS

## Step-by-step guide

### 1. Create an auth error logging table

Start by creating a table to store auth errors. The table should capture the error type, message, user email (if available), the timestamp, and any additional context. Use the service_role key for inserts so that errors can be logged even when the user is not authenticated. Enable RLS but create a policy that allows the service role to bypass it.

```
-- Create the auth error logs table
CREATE TABLE public.auth_error_logs (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  error_type TEXT NOT NULL,
  error_message TEXT NOT NULL,
  error_code TEXT,
  user_email TEXT,
  user_id UUID,
  ip_address TEXT,
  user_agent TEXT,
  metadata JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Enable RLS
ALTER TABLE public.auth_error_logs ENABLE ROW LEVEL SECURITY;

-- Allow authenticated admins to read logs
CREATE POLICY "Admins can read auth error logs"
ON public.auth_error_logs FOR SELECT
TO authenticated
USING (
  (SELECT auth.jwt() ->> 'role') = 'admin'
);

-- Allow inserts via Edge Function (service role bypasses RLS)
-- No INSERT policy needed when using service_role key
```

**Expected result:** The auth_error_logs table is created with RLS enabled. Only admin users can read logs, and inserts happen through server-side code.

### 2. Create an Edge Function for secure error logging

Build an Edge Function that receives auth error details and inserts them into the logging table using the service_role key. This is more secure than inserting directly from the client because it prevents users from spoofing log entries. The function validates the input, adds server-side metadata like timestamps, and stores the record.

```
// supabase/functions/log-auth-error/index.ts
import { corsHeaders } from '../_shared/cors.ts';
import { createClient } from 'npm:@supabase/supabase-js@2';

Deno.serve(async (req) => {
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders });
  }

  try {
    const { error_type, error_message, error_code, user_email, metadata } =
      await req.json();

    // Create client with service_role to bypass RLS
    const supabase = createClient(
      Deno.env.get('SUPABASE_URL')!,
      Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
    );

    const { error } = await supabase.from('auth_error_logs').insert({
      error_type,
      error_message,
      error_code: error_code || null,
      user_email: user_email || null,
      user_agent: req.headers.get('user-agent'),
      metadata: metadata || {},
    });

    if (error) throw error;

    return new Response(
      JSON.stringify({ logged: true }),
      { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    );
  } catch (err) {
    return new Response(
      JSON.stringify({ error: err.message }),
      { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 500 }
    );
  }
});
```

**Expected result:** The Edge Function accepts error details via POST and stores them securely in the auth_error_logs table.

### 3. Capture client-side auth errors and send them to the logging function

Wrap your auth calls in error-handling logic that sends failures to the logging Edge Function. Capture errors from signInWithPassword, signUp, signOut, and the onAuthStateChange listener. Include as much context as possible — the error message, code, attempted email, and the action that failed.

```
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

async function logAuthError(
  errorType: string,
  errorMessage: string,
  errorCode?: string,
  email?: string
) {
  try {
    await supabase.functions.invoke('log-auth-error', {
      body: {
        error_type: errorType,
        error_message: errorMessage,
        error_code: errorCode,
        user_email: email,
        metadata: { page: window.location.pathname },
      },
    });
  } catch (e) {
    console.error('Failed to log auth error:', e);
  }
}

// Wrap sign-in with error logging
async function signIn(email: string, password: string) {
  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password,
  });

  if (error) {
    await logAuthError('sign_in_failed', error.message, error.status?.toString(), email);
    throw error;
  }

  return data;
}
```

**Expected result:** Failed auth attempts are automatically logged to the database via the Edge Function.

### 4. Monitor auth events with onAuthStateChange

The onAuthStateChange listener fires for all auth events including TOKEN_REFRESHED and PASSWORD_RECOVERY. Listen for error events and log them. This catches errors that happen outside of explicit sign-in calls, such as token refresh failures when a session expires.

```
// Set up global auth event monitoring
supabase.auth.onAuthStateChange(async (event, session) => {
  // Log specific events that indicate problems
  if (event === 'TOKEN_REFRESHED' && !session) {
    await logAuthError(
      'token_refresh_failed',
      'Session token refresh returned no session',
      undefined,
      undefined
    );
  }

  if (event === 'SIGNED_OUT') {
    // Could be intentional or forced — log for audit trail
    console.log('User signed out');
  }

  // Log all events for audit purposes
  console.log('Auth event:', event, session?.user?.email);
});
```

**Expected result:** Auth events are monitored in real time, and errors like token refresh failures are logged automatically.

### 5. Query auth error logs for debugging and security analysis

Use SQL queries to analyze your auth error logs. Look for patterns like repeated failed logins from the same email (potential brute force), spikes in signup failures (potential configuration issues), and token refresh errors (potential session management problems). Create a simple admin view to monitor these patterns.

```
-- Find the most common auth errors in the last 24 hours
SELECT error_type, error_message, COUNT(*) as occurrences
FROM auth_error_logs
WHERE created_at > now() - INTERVAL '24 hours'
GROUP BY error_type, error_message
ORDER BY occurrences DESC;

-- Detect potential brute force attempts (5+ failures per email)
SELECT user_email, COUNT(*) as failed_attempts,
  MIN(created_at) as first_attempt,
  MAX(created_at) as last_attempt
FROM auth_error_logs
WHERE error_type = 'sign_in_failed'
  AND created_at > now() - INTERVAL '1 hour'
GROUP BY user_email
HAVING COUNT(*) >= 5
ORDER BY failed_attempts DESC;

-- Recent errors with full details
SELECT error_type, error_message, user_email, user_agent, created_at
FROM auth_error_logs
ORDER BY created_at DESC
LIMIT 50;
```

**Expected result:** You can identify patterns in auth failures, detect potential security threats, and debug specific user issues from the logs.

## Complete code example

File: `auth-error-logger.ts`

```typescript
// Auth error logging utility for Supabase
// Captures and stores auth errors for debugging and security monitoring

import { createClient, AuthError } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

interface AuthErrorLog {
  error_type: string;
  error_message: string;
  error_code?: string;
  user_email?: string;
  metadata?: Record<string, unknown>;
}

async function logAuthError(log: AuthErrorLog): Promise<void> {
  try {
    await supabase.functions.invoke('log-auth-error', {
      body: {
        ...log,
        metadata: {
          ...log.metadata,
          page: typeof window !== 'undefined' ? window.location.pathname : 'server',
          timestamp: new Date().toISOString(),
        },
      },
    });
  } catch (e) {
    console.error('Failed to log auth error:', e);
  }
}

// Wrapped auth methods with automatic error logging
export async function signIn(email: string, password: string) {
  const { data, error } = await supabase.auth.signInWithPassword({ email, password });
  if (error) {
    await logAuthError({
      error_type: 'sign_in_failed',
      error_message: error.message,
      error_code: String(error.status || ''),
      user_email: email,
    });
    throw error;
  }
  return data;
}

export async function signUp(email: string, password: string) {
  const { data, error } = await supabase.auth.signUp({ email, password });
  if (error) {
    await logAuthError({
      error_type: 'sign_up_failed',
      error_message: error.message,
      error_code: String(error.status || ''),
      user_email: email,
    });
    throw error;
  }
  return data;
}

export async function signOut() {
  const { error } = await supabase.auth.signOut();
  if (error) {
    await logAuthError({
      error_type: 'sign_out_failed',
      error_message: error.message,
    });
    throw error;
  }
}

// Initialize global auth event monitoring
export function initAuthMonitoring() {
  supabase.auth.onAuthStateChange(async (event, session) => {
    if (event === 'TOKEN_REFRESHED' && !session) {
      await logAuthError({
        error_type: 'token_refresh_failed',
        error_message: 'Token refresh returned no session',
      });
    }
  });
}
```

## Common mistakes

- **Creating a client-side INSERT policy on the auth_error_logs table, allowing users to write fake log entries** — undefined Fix: Route all log inserts through an Edge Function using the service_role key. Deploy the function with --no-verify-jwt so unauthenticated users' errors can be logged.
- **Logging passwords or sensitive session tokens in the error metadata** — undefined Fix: Only log error messages, error codes, email addresses, and non-sensitive context. Never include passwords, tokens, or API keys in log entries.
- **Not cleaning up old auth error logs, causing the table to grow indefinitely** — undefined Fix: Set up a pg_cron job to delete entries older than 90 days, or create a retention policy based on your compliance requirements.

## Best practices

- Route all auth error log inserts through a server-side Edge Function to prevent spoofed entries
- Never log passwords or authentication tokens — only log error messages, codes, and email addresses
- Monitor for brute force patterns by querying failed login counts per email address per hour
- Set up automated cleanup of old log entries using pg_cron to manage table size
- Include page context in error logs to identify which part of your app generates the most auth errors
- Use the onAuthStateChange listener to catch errors that happen outside explicit auth calls
- Create a simple admin dashboard to review auth error patterns in real time
- Deploy the logging Edge Function with --no-verify-jwt to capture errors from unauthenticated users

## Frequently asked questions

### Does Supabase log auth errors by default?

Supabase logs API requests in the Dashboard under Logs, but these logs are not persistent or easily queryable for auth-specific errors. Building a custom logging table gives you full control over retention, querying, and alerting.

### Should I log auth errors on the client or the server?

Both. Client-side logging captures errors with user context like the page they were on. Server-side logging via Edge Functions is more secure and ensures errors are captured even if the client fails to send them.

### How do I detect brute force login attempts from the logs?

Query the auth_error_logs table for emails with 5+ failed sign_in attempts within a short time window (e.g., 1 hour). Use this data to trigger account lockouts or CAPTCHA challenges.

### Will logging auth errors slow down my application?

No. The logging call is asynchronous and non-blocking. Even if the Edge Function is slow or fails, the user's auth experience is not affected because the log call happens in the background.

### How long should I retain auth error logs?

This depends on your compliance requirements. A typical retention period is 90 days for debugging purposes. Security-sensitive applications may require 1+ years. Use pg_cron to automate cleanup.

### Can I get alerts when auth errors spike?

Yes. Create a pg_cron job that checks for error spikes periodically and sends a notification via a database webhook or Edge Function to your Slack or email when thresholds are exceeded.

### Can RapidDev help build a security monitoring system for my Supabase project?

Yes. RapidDev can build comprehensive auth monitoring solutions including error logging, brute force detection, real-time alerting, and admin dashboards for your Supabase application.

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-log-auth-errors-in-supabase
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-log-auth-errors-in-supabase
