The traditional auth flow is a rendering bottleneck. User arrives at /dashboard. Your server checks if they're logged in: fetch the request cookies, query Supabase (30–100ms), parse the response, check permissions, then render. The user waits. By the time your page appears, the server has round-tripped the database and the browser is CPU-starved catching up. Move the auth check to the edge—a Cloudflare Worker or Netlify Edge Function that runs *before* the request hits your origin—and you gate pages in 5ms instead of 50ms. The pattern: verify Supabase JWTs using the public JWKS (no database needed), decode the session, cache it in a cookie, redirect unauth'd users to login. The overhead? Negligible. The speed gain? Measurable.
Why Edge Auth Beats Server-Side Auth
Your origin is the slowest point in the request chain. It's where your database lives, where your code executes, where latency compounds. An edge function runs in a global network close to the user—Cloudflare has 300+ data centers worldwide, Netlify Edge runs on Cloudflare. Latency from edge to user: 5–20ms. Latency from origin to edge: 30–200ms depending on geography. The math: edge auth adds 5ms, saves 50ms by not hitting origin. The 45ms win is user-visible. For /login, /pricing, /blog, and protected pages, edge auth is the difference between instant and sluggish.
And because edge functions don't scale differently than your origin (both handle per-request work), edge auth costs nothing in compute. You're not adding a second infrastructure burden—you're reordering the work that was already happening.
How JWKS Verification Works (No Database)
Supabase uses public-key cryptography. When a user logs in, Supabase's backend signs the JWT with a private key. Your edge function verifies the signature using Supabase's public key (the JWKS endpoint), which is publicly available. No database lookup, no API call to Supabase. Just math.
Step 1: Fetch the Public JWKS
// JWKS = JSON Web Key Set. It's public.
// Endpoint: https://<project-id>.supabase.co/.well-known/jwks.json
fetch('https://YOUR_PROJECT.supabase.co/.well-known/jwks.json')
.then(res => res.json())
.then(jwks => {
// jwks.keys is an array of public keys
// One of them signed the JWT you're verifying
})
Step 2: Extract the JWT from the Request
// JWT is in the Authorization header (APIs, CLIs)
// OR in a cookie (browsers with Supabase Auth)
const token = request.headers.get('Authorization')?.replace('Bearer ', '')
|| getCookieValue(request, 'sb-access-token');
// Token looks like: eyJhbGc...eyJz....signature
Step 3: Verify the Signature
// Use a JWT library (e.g., jsonwebtoken, jose)
import * as jose from 'jose';
const jwks = jose.createRemoteJWKSet(
new URL('https://YOUR_PROJECT.supabase.co/.well-known/jwks.json')
);
const verified = await jose.jwtVerify(token, jwks);
// verified.payload contains user_id, email, etc.
// If the token is fake or expired, jwtVerify() throws.
// No database needed.
The Edge Auth Pattern: Middleware Code
Netlify Edge Functions (Deploy to Edge Instantly)
// netlify/edge-functions/auth.ts
import * as jose from 'jose';
const SUPABASE_URL = Deno.env.get('SUPABASE_URL')!;
const PROJECT_ID = SUPABASE_URL.split('//')[1].split('.')[0];
const JWKS_URL = `${SUPABASE_URL}/.well-known/jwks.json`;
const jwks = jose.createRemoteJWKSet(new URL(JWKS_URL));
export default async (request: Request) => {
const url = new URL(request.url);
// Skip auth check for public paths
if (['/login', '/signup', '/pricing'].includes(url.pathname)) {
return undefined; // Let the request pass through
}
// Protected routes require valid JWT
const token = request.headers.get('Authorization')?.replace('Bearer ', '')
|| getCookieValue(request, 'sb-access-token');
if (!token) {
return new Response(null, {
status: 302,
headers: { Location: '/login' }
});
}
try {
const verified = await jose.jwtVerify(token, jwks);
// Store decoded session in request context for your Netlify function
request.session = verified.payload;
return undefined; // Request passes through, verified
} catch (err) {
// Token is invalid or expired
return new Response(null, {
status: 302,
headers: { Location: '/login' }
});
}
};
function getCookieValue(request: Request, name: string): string | null {
const cookies = request.headers.get('cookie')?.split('; ') || [];
const cookie = cookies.find(c => c.startsWith(`${name}=`));
return cookie?.split('=')[1] || null;
}
Cloudflare Workers (Same Pattern, Different API)
// src/index.ts (Wrangler project)
import * as jose from 'jose';
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
// Public paths bypass auth
if (['/login', '/signup', '/pricing'].includes(url.pathname)) {
return fetch(request);
}
// Extract JWT from Authorization header or cookie
const token = request.headers.get('Authorization')?.replace('Bearer ', '')
|| getCookieValue(request, 'sb-access-token');
if (!token) {
return new Response(null, {
status: 302,
headers: { Location: '/login' }
});
}
try {
const jwks = jose.createRemoteJWKSet(
new URL(`${env.SUPABASE_URL}/.well-known/jwks.json`)
);
await jose.jwtVerify(token, jwks);
// Token valid: forward to origin with headers
const headers = new Headers(request.headers);
headers.set('X-Verified-User', 'true');
return fetch(request, { headers });
} catch (err) {
return new Response(null, {
status: 302,
headers: { Location: '/login' }
});
}
}
};
function getCookieValue(request: Request, name: string): string | null {
const cookies = request.headers.get('cookie')?.split('; ') || [];
const cookie = cookies.find(c => c.startsWith(`${name}=`));
return cookie?.split('=')[1] || null;
}
Caching the Decoded Session (< 100ms Repeat Checks)
JWKS verification takes 5–10ms the first time (fetching the key set), 1ms on repeat (cached). But you can optimize further: after verifying the JWT, encode the decoded session into a response cookie. Then, on the next request, decode the cookie instead of verifying the JWT every time. Trade: you bypass a signature check, but you still validate the signature once per session lifetime (or per refresh token rotation).
// After verification succeeds:
const sessionPayload = JSON.stringify(verified.payload);
// Set a cookie with the encoded session (not the JWT itself)
const setCookieHeader = `X-Session=${encodeURIComponent(sessionPayload)}; Path=/; HttpOnly; Secure`;
// On the next request:
const sessionCookie = getCookieValue(request, 'X-Session');
if (sessionCookie) {
const session = JSON.parse(decodeURIComponent(sessionCookie));
// No JWKS fetch, just parse. ~0.1ms.
}
// Invalidate this cookie when the JWT refreshes (logout, role change).
Production Checklist: Edge Auth at Scale
1. Cache the JWKS Locally (5–30s TTL)
Fetching the JWKS from Supabase every request is waste. Cloudflare Workers and Netlify Edge have built-in caching. Cache the JWKS for 5–30 seconds. If Supabase rotates keys (rare, but possible), you pick up the new key within the TTL.
2. Short JWT Expiry (5–15 min)
If a JWT leaks, the attacker has a time window. Short expiry (5 min) narrows that window. Pair with a refresh token (longer expiry, HTTPOnly cookie) that the client uses to get a new access token. Supabase does this by default.
3. Redirect Unauth'd Users to /login, Not /error
Missing or invalid tokens → 302 redirect to /login. Don't return 401 and let the browser hit origin; redirect at the edge and save the round-trip.
4. Whitelist Public Paths (Don't Over-Gate)
Blog, pricing, login, signup, legal pages don't need auth. Return undefined (Netlify Edge) or fetch(request) (Cloudflare) for public paths so they skip auth checks.
5. Handle Token Refresh Gracefully
When the access token expires, the edge rejects it and redirects to /login. But if the user has a valid refresh token, they should be silently logged back in. On the login page, call /auth/refresh to get a new access token, then redirect to /dashboard. Supabase handles this in the client library.
Six Edge Auth FAQs
Does edge auth work with server-side rendering?
Yes. The edge auth check runs before your server renders. If the edge redirects to /login, the server never sees the request. If the auth passes, you can forward the verified user info to your server (e.g., via a header). Your server can then use that info to render user-specific content.
What if JWKS verification fails due to network?
Edge functions have local caches and fallback networks. But if you're paranoid (smart), cache the JWKS locally the first time, and if the fetch fails on a repeat check, use the cached key set. Stale keys are safer than no keys.
Can I use edge auth with session cookies?
Yes. Instead of JWTs in Authorization headers, verify the session cookie (by looking up the session ID in a KV store like Cloudflare Workers KV or Deno KV). This is more complex but works for traditional server-rendered apps.
Does edge auth slow down public pages?
No. You whitelist public paths (like /blog, /pricing) and skip auth checks for them. Unprotected pages pass through instantly.
What happens if a user is logged out server-side but their token is still valid?
The JWT doesn't know it's been revoked until it expires. This is the stateless token tradeoff. To fix it, either (a) use short expiry times (5 min) so revocation is effective quickly, or (b) check a blocklist/revocation list at the edge (but then you're hitting a KV store, which adds latency). Most apps accept the tradeoff and use short expiry.
Can I use edge auth to gate APIs?
Yes. The same pattern applies: verify JWTs in Authorization headers, check expiry and claims, redirect or return 401. APIs don't redirect (browsers do), so return new Response('Unauthorized', { status: 401 }) instead.
The Bottom Line
Auth checks at the server add latency. Move them to the edge, verify JWTs using Supabase's public JWKS, and you gate pages in 5ms instead of 50ms. The pattern is simple: extract the token, verify the signature using the key set, cache the result, and redirect unauth'd users. No database calls, no extra infrastructure. Netlify Edge Functions and Cloudflare Workers make this trivial to deploy. For more on JWT vs session cookies, see JWT vs session cookies: the trade-offs explained. Ready to ship edge-speed auth? Hit Aidxn Design's backend partnerships to get it done.