Updating to the new Supabase Asymmetric Keys was a Developer's Dream

Updating to the new Supabase Asymmetric Keys was a Developer's Dream

Updating to the new Supabase Asymmetric Keys was a Developer's Dream

Ed Morales

Ed Morales

Product

5

min read

Dec 31, 2025

Supabase's move to JWT Signing Keys (asymmetric JWT signing + JWKS) is one of those upgrades that hits the rare trifecta: better security, smoother operations, and faster apps.

What made it feel like a developer's dream wasn't just the technical change, it was the whole experience: the docs are clear, the rollout is built for real-world migrations, and the platform makes key rotation feel boring (in the best way).

If you haven't read them yet, start here:


The parts that impressed us the most:

1) Migration without fear

The biggest "wow" is that Supabase designed this so you can migrate and rotate without breaking everyone's sessions:

  • Dual compatibility during migration: you can keep existing JWT behavior working while you adopt the new signing keys flow.

  • Rotation without downtime: rotate keys, keep existing (non-expired) tokens valid, and move forward safely.

  • Operational fallback: in practice, this means you can ship support for the new keys first, rotate second, and still have a clean escape hatch.

That sequencing matters. It's what makes it feel safe to do in production.

And the best part? If you do it right, the migration is… boring. It's almost anticlimactic: you deploy support for the new keys, rotate, and nothing explodes. That's not luck, that's a well-designed migration path.


2) Performance that's easy to actually use

The docs and examples make it clear that the new model enables local verification via signing keys / JWKS, which reduces the need for round-trips just to "learn who the user is".

In a Next.js App Router world, that's huge:

  • Middleware and Server Components can run a lot.

  • If every request path requires a network verification call, you pay for it in latency and reliability.

  • If you can safely verify claims locally for most paths, things get snappier fast.

Supabase's getClaims() approach is what made the "fast path" feel ergonomic, not risky.


What the performance gains look like in practice

Here's what you typically win when you shift routine auth checks to claims verification:

  • Fewer network round-trips on the "every request" paths (middleware, layout gating, server components).

  • Lower tail latency: fewer dependencies on a remote auth verification call when you just need identity/claims to render.

  • More resilient UX: fewer "auth hiccups" where a transient network issue turns into "logged out" UI.

  • Cleaner layering: use the fast path for "who are you?", keep the verified path for "are you allowed to do this?".

In Next.js, that's the difference between auth being a constant tax and auth being basically invisible.


Check out this great tutorial video for a complete breakdown

As usual, Jon Meyers from Supabase put together a great video tutorial to demystify the process and show you exactly how easy it is:


The auth setup we recommend in Next.js

Dreambase's auth strategy became intentionally hybrid:

  • API Routes: use DB-verified auth (supabase.auth.getUser()) for authorization-critical operations (mutations, permission-gated reads).

  • Middleware / Server Components / Server Actions: use a fast, claims-based helper backed by supabase.auth.getClaims().


1) Env vars: prefer new keys, keep legacy fallbacks (during migration)

On the client, accept either the new publishable key or the anon key as a fallback:

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabasePublishableOrAnon =
  process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY ??
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;

if (!supabaseUrl) throw new Error("Missing NEXT_PUBLIC_SUPABASE_URL");
if (!supabasePublishableOrAnon)
  throw new Error(
    "Missing NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY (preferred) or NEXT_PUBLIC_SUPABASE_ANON_KEY (fallback)"
  );

On the server, we prefer the new secret key, with service-role as fallback:

const supabaseSecretOrServiceRole =
  process.env.SUPABASE_SECRET_KEY ?? process.env.SUPABASE_SERVICE_ROLE_KEY;

if (!supabaseSecretOrServiceRole)
  throw new Error(
    "Missing SUPABASE_SECRET_KEY (preferred) or SUPABASE_SERVICE_ROLE_KEY (fallback)"
  );

This is the exact "both sets of keys work" property you want during rollout and rotation.


2) Fast path: getClaims() → typed "authenticated user"

Create a tiny helper that turns JWT claims into the minimal user object you need to render UI and gate routes.

export const getUser = async (
  supabase?: SupabaseClient<Database>
): Promise<{ user: AuthenticatedUser | null; error: string | null }> => {
  const client = supabase ?? (await createClient());
  const { data: claimsData } = await client.auth.getClaims();
  return convertJwtPayloadToAuthenticatedUser(claimsData?.claims);
};

And we reused it in middleware to avoid doing heavier auth work just to decide whether someone is authenticated to see a route:

import { NextResponse, type NextRequest } from "next/server";

export async function middleware(req: NextRequest) {
  const supabase = /* createServerClient(...) */;
  const user = await getUserFromClaims(supabase);

  const isProtected =
    req.nextUrl.pathname.startsWith("/dashboard") ||
    req.nextUrl.pathname.startsWith("/workspace");

  if (isProtected && !user) {
    return NextResponse.redirect(new URL("/auth/login", req.url));
  }
  return NextResponse.next();
}


3) Security still matters: API routes stay DB-verified

For the parts of the app that mutate data and enforce RBAC, we kept the stronger, DB-verified call in API routes:

import { NextResponse } from "next/server";

export async function POST(req: Request) {
  const supabase = /* createServerClient(...) */;
  const {
    data: { user },
    error,
  } = await supabase.auth.getUser();

  if (error || !user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  // ...perform mutation using user.id (and enforce authorization)...
  return NextResponse.json({ ok: true });
}

That "fast path where safe, verified path where required" split is the real win in practice.


Recommended migration checklist (practical + low drama):

Based on the Supabase rollout model:

  • Enable JWT Signing Keys in Supabase and read through the rotation/migration steps (signing keys guide).

  • Ship code that supports the new keys first:

    • Add NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY (client/shared).

    • Add SUPABASE_SECRET_KEY (server-only).

    • Keep NEXT_PUBLIC_SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY as fallbacks until you're confident.

  • Separate auth "fast path" from "verified path":

    • Use claims-based helpers (getClaims()) for UI gating and non-critical user hydration.

    • Keep DB verification (getUser()) for API routes and sensitive mutations.

  • Rotate keys when your deployments are live and stable, then monitor.

  • Only then revoke the legacy secret once you're satisfied everything is issuing/verifying as expected.


Why this felt like a "developer's dream"

The standout is that Supabase treated this like a production migration, not a breaking change:

  • Great docs + concrete workflow

  • Amazing tutorial videos that are incredibly easy to follow

  • Migration and rotation designed around compatibility and confidence

  • A very practical path to performance wins in modern frameworks

If you're building on Nextjs and Supabase, this upgrade is one of the few that genuinely feels like: "ship it, rotate it, forget it."

A key rotation shouldn't be a launch. It should be a boring Tuesday maintenance task you barely remember.

Ready to witness a new world in AI-native analytics where YOU take the driver's seat?

RELATED BLOGS

More updates from the Dream Team

RELATED BLOGS

More updates from the Dream Team

RELATED BLOGS

More updates from the Dream Team

Join our newsletter

Keep up with the exciting developments from the Dream Team!

Join our newsletter

Keep up with the exciting developments from the Dream Team!

Join our newsletter

Keep up with the exciting developments from the Dream Team!