Skip to content
Back to skills

Auth Patterns

ASecurity

Supabase auth can cause hangs and deadlocks due to the Web Locks API.

  • 18 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 4, 2026
developmenttypescriptgoreactnextjsdebuggingapidatabase

Works with

  • claude code
  • cursor
  • cli
  • api
  • mcp

Security analysis

A100/100

Scanned September 4, 2026

npx -y skills add majiayu000/claude-skill-registry-data --skill auth-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Auth Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Auth Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/majiayu000-auth-patterns-claude-skill-registry-data/badge)](https://www.skillsdirectory.com/skills/majiayu000-auth-patterns-claude-skill-registry-data)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: auth-patterns
description: Supabase auth can cause hangs and deadlocks due to the Web Locks API.
  This skill covers safe patterns to avoid these issues.
---

---
name: auth-patterns
description: Supabase authentication patterns including getUser vs getSession, deadlock avoidance, session handling, and bypass patterns. Use when working with auth, sessions, cookies, or encountering auth hangs/timeouts. Keywords: auth, getUser, getSession, session, deadlock, timeout, cookie, token, Web Locks.
compatibility: Antigravity, Claude Code, Cursor
metadata:
  version: "1.0"
  project: "stepleague"
---

# Auth Patterns Skill

## Overview

Supabase auth can cause hangs and deadlocks due to the Web Locks API. This skill covers safe patterns to avoid these issues.

---

## ⚠️ Critical: getUser vs getSession

### Decision Flowchart

```
Need user info?
├─ Server-side (API route)?
│  └─ Use getUser() → More reliable, validates with server
├─ Client-side initial load?
│  └─ Use onAuthStateChange → Avoids deadlock
├─ Client-side after load?
│  └─ Use cached session from context
└─ Background/long-running task?
   └─ Parse cookie directly → No locks!
```

### Quick Reference

| Method | When to Use | Risk Level |
|--------|-------------|------------|
| `getUser()` | API routes, server components | Low - validates token |
| `getSession()` | ❌ Avoid on client | **HIGH - Can deadlock** |
| `onAuthStateChange` | Client-side initial load | Safe |
| Cookie parsing | Background tasks, fallback | Safe - no locks |

---

## Pattern 1: API Routes (Safe)

```typescript
import { createServerSupabaseClient } from "@/lib/supabase/server";

export async function GET(req: Request) {
  const supabase = await createServerSupabaseClient();

  // ✅ getUser is safe on server - validates with Supabase
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) {
    return new Response('Unauthorized', { status: 401 });
  }

  // Continue with user...
}
```

---

## Pattern 2: Client-Side Initial Load (Safe)

```typescript
// ✅ CORRECT - Use onAuthStateChange
useEffect(() => {
  const { data: { subscription } } = supabase.auth.onAuthStateChange(
    (event, session) => {
      if (event === 'INITIAL_SESSION') {
        setSession(session);
        setLoading(false);
      }
    }
  );

  return () => subscription.unsubscribe();
}, []);
```

**Why?** `onAuthStateChange` doesn't acquire Web Locks like `getSession()` does.

---

## Pattern 3: Avoid getSession on Client

```typescript
// ❌ WRONG - Can deadlock!
const handleClick = async () => {
  const { data: { session } } = await supabase.auth.getSession();
  // This can hang forever...
};

// ✅ CORRECT - Use session from context
const { session } = useAuth();
const handleClick = async () => {
  if (!session) return;
  // Use cached session
};
```

---

## Pattern 4: Background/Long-Running Tasks

For batch operations or tasks that run in the background:

```typescript
import { createClient as createArgsClient } from "@supabase/supabase-js";

// Parse token directly from cookie - NO LOCKS
function getTokenFromCookie(): string | null {
  const cookie = document.cookie
    .split('; ')
    .find(c => c.startsWith('sb-'));

  if (!cookie) return null;

  const value = decodeURIComponent(cookie.split('=')[1]);
  const parsed = JSON.parse(value);
  return parsed?.access_token;
}

// Create stateless client - SAFE
const token = getTokenFromCookie();
const tempClient = createArgsClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
  {
    global: { headers: { Authorization: `Bearer ${token}` } },
    auth: { persistSession: false, autoRefreshToken: false }
  }
);

// Use for background operations
await tempClient.from("submissions").insert({ ... });
```

---

## Pattern 5: Token Expiry Validation

Always check expiry before using cached tokens:

```typescript
function isTokenValid(token: string): boolean {
  try {
    const payload = JSON.parse(atob(token.split('.')[1]));
    const expiresAt = payload.exp * 1000;
    const bufferMs = 60 * 1000; // 1 minute buffer

    return Date.now() < (expiresAt - bufferMs);
  } catch {
    return false;
  }
}

// Usage
const token = getTokenFromCookie();
if (!token || !isTokenValid(token)) {
  redirectToSignIn();
  return;
}
```

---

## Pattern 6: Session Timeout Handling

For long operations (batch uploads):

```typescript
const SESSION_TIMEOUT_MS = 5000;

async function withSessionTimeout<T>(
  operation: () => Promise<T>
): Promise<T> {
  return Promise.race([
    operation(),
    new Promise<never>((_, reject) =>
      setTimeout(() => reject(new Error('Session timeout')), SESSION_TIMEOUT_MS)
    )
  ]);
}

// Usage
try {
  const user = await withSessionTimeout(() => supabase.auth.getUser());
} catch (e) {
  if (e.message === 'Session timeout') {
    // Use cookie fallback
    const token = getTokenFromCookie();
    // ...
  }
}
```

---

## StepLeague Implementation

### Key Files

| File | Purpose |
|------|---------|
| `src/lib/auth/sessionCache.ts` | Cookie parsing, token validation |
| `src/components/providers/AuthProvider.tsx` | onAuthStateChange pattern |
| `src/lib/api/client.ts` | Timeout wrapper for getUser |

### The Auth Flow

```
1. Page loads
   └─ AuthProvider uses onAuthStateChange (safe)

2. Session received
   └─ Cached in React context

3. API calls
   └─ Use cached session, not getSession()

4. Long operations
   └─ Use cookie parsing fallback

5. Session expires
   └─ Token refresh via onAuthStateChange
```

---

## Debugging Auth Issues

### Symptom: Page stuck on "Loading..."

**Causes:**
1. `getSession()` deadlock
2. onAuthStateChange never firing

**Fix:**
1. Check if using getSession on client
2. Add timeout/fallback to AuthProvider
3. Check `/reset` page to clear stale state

### Symptom: "Session timeout" in batch operations

**Cause:** Web Locks held too long

**Fix:** Use cookie parsing fallback (Pattern 4)

---

## Pattern 7: Cookie Sync-Back Prevention

### The Problem

Supabase SSR (`@supabase/ssr`) automatically syncs auth cookies from localStorage:

```
User deletes cookie
↓
Supabase detects missing cookie
↓
Reads session from localStorage
↓
Restores cookie automatically
↓
Result: Cookie reappears ❌
```

### The Fix: Atomic Storage + Cookie Clear

When clearing auth state, **ALWAYS** clear localStorage BEFORE clearing cookies:

```typescript
// ❌ WRONG ORDER
document.cookie = 'sb-xxx=; expires=Thu, 01 Jan 1970...';  // Clear cookie first
localStorage.clear();  // Too late! Cookie already synced back

// ✅ CORRECT ORDER
localStorage.clear();  // Clear storage FIRST
sessionStorage.clear();
// THEN clear cookies
document.cookie = 'sb-xxx=; expires=Thu, 01 Jan 1970...';
```

### Usage in /reset Page

```typescript
// src/app/reset/page.tsx
const performReset = async () => {
  // 1. Storage FIRST (prevents sync-back)
  localStorage.clear();
  sessionStorage.clear();

  // 2. IndexedDB (Supabase may store here too)
  const dbs = await indexedDB.databases();
  await Promise.all(dbs.map(db => indexedDB.deleteDatabase(db.name)));

  // 3. Cookies (storage already gone, can't sync)
  document.cookie.split(";").forEach(c => {
    const name = c.split("=")[0].trim();
    document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
  });

  // 4. Caches last
  const cacheNames = await caches.keys();
  await Promise.all(cacheNames.map(c => caches.delete(c)));

  // 5. Hard redirect (bypass client router)
  window.location.href = "/sign-in?reset=true";
};
```

### Why This Matters

- Manual cookie deletion won't work if localStorage has session
- `/reset` page must clear in correct order
- Sign-out flows must clear both storage + cookies atomically
- Chrome DevTools cookie deletion triggers sync-back if localStorage exists

### Related Patterns

- Pattern 4: Background Tasks (cookie parsing)
- Pattern 5: Token Expiry Validation

---

## Related Skills

- `supabase-patterns` - Database operations, MCP usage, RLS *(auth section references this skill)*
- `middleware-patterns` - Auth checks in Next.js middleware
- `api-handler` - Server-side auth in API routes

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…