name: supabase-auth-rest-nodejs
description: "Bypass Supabase JS client for auth in Node.js 18+ — use REST API fetch instead of createClient.getUser() to avoid RealtimeClient/WebSocket crashes"
category: devops
Supabase Auth REST API Pattern (Node.js)
Problem
createClient from @supabase/supabase-js always initializes RealtimeClient internally, even with realtime: false. RealtimeClient unconditionally imports ws (the npm package), which fails on Node.js 18+ because:
- - The
wsmodule doesn't auto-inject into the module scope unless the bundler handles it - - Or: "Node.js 20 detected without native WebSocket support" error
- -
realtime: falseevaluates to{}via spread — RealtimeClient is still constructed - -
transport: { WebSocket }injectswsvia__supabase_ws_via_moduleglobal, but RealtimeClient constructor also doesimport { WebSocket } from "ws"which fails in Node.js ESM context - - Even stubbing
realtime: { subscribe: () => ({ unsubscribe: () => {} }) }fails —@supabase/realtime-jsstill throws:"Node.js 20 detected without native WebSocket support"at import/initialization time - - The only clean fix is removing all Supabase client usage for auth — use either
jwt.verify(local, recommended) or REST API fetch (network) - - Solution 1 (
jwt.verify): Fastest, no network call, works in any Node.js context. Use whenJWT_SECRETis available in.env. - - Solution 2 (REST API): Use when you need full user profile data beyond just the user ID, or when
JWT_SECRETis not available. - - TanStack Start / Node.js server-side route handlers
- - Any Node.js backend that needs to validate Supabase Auth tokens
- - Avoids the entire
createClient+ RealtimeClient dependency chain - - Upload route example:
/var/www/documentos/deploy-vps-nodejs/app/src/routes/api/public/upload-document.ts
Result: auth.getUser(token) throws inside any TanStack Start route handler, returning {"status":500,"unhandled":true,"message":"HTTPError"} — the actual error is buried in server logs.
Solution 1: Local JWT verification (recommended)
Most efficient — no network call, no Supabase client needed at all. Works for any Supabase token (Google OAuth, email, etc.) as long as you have JWT_SECRET from .env.
`typescript
import jwt from "jsonwebtoken";
const token = authHeader.replace("Bearer ", "");
const JWT_SECRET = process.env.JWT_SECRET!;
const decoded = jwt.verify(token, JWT_SECRET) as { sub?: string; user_id?: string };
const userId = decoded.sub || decoded.user_id || "";
if (!userId) {
return new Response(JSON.stringify({ error: "Sessão inválida" }), { status: 401 });
}
`
Where to find JWT_SECRET: In your Supabase project dashboard → Project Settings → API → "JWT Secret". In .env it's the JWT_SECRET= line.
Solution 2: Direct REST API for Auth
Falls back to Supabase Auth REST API when you don't have JWT_SECRET or need full user profile data:
`typescript
const SB_URL = process.env.SUPABASE_URL;
const SB_SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
const res = await fetch(${SB_URL}/auth/v1/user, {
headers: {
Authorization: Bearer ${token},
apikey: SB_SERVICE_KEY,
},
});
if (!res.ok) {
throw new Error(Sessão inválida: ${res.status});
}
const userData = await res.json();
const userId = userData.id;
`
**Why Solution 1 (jwt.verify) is preferred
Confirmed broken patterns (do NOT use):
`typescript
// ❌ Does NOT work — RealtimeClient still initializes and throws WebSocket error
const supabase = createClient(URL, KEY, {
realtime: { subscribe: () => ({ unsubscribe: () => {} }) },
auth: { autoRefreshToken: false, persistSession: false }
});
`
Error: "Node.js 20 detected without native WebSocket support." — even in Node.js 20 this error appears because the RealtimeClient module-level code checks for native WebSocket and throws before any option can take effect.
Confirmed working patterns:
1. ✅ jwt.verify(token, JWT_SECRET) — local, no network, fastest
2. ✅ REST fetch to /auth/v1/user with service role apikey — network call, works but slower
Google OAuth tokens and sub claim
Supabase Auth Google OAuth tokens DO include sub in the JWT payload (it's the Supabase user ID). Solution 1 (jwt.verify) works correctly with these tokens. The REST API (Solution 2) also works and returns the full user object including id.