Idra .

Building Auth in Next.js with Supabase: What I Learned This Weekend

| November 15, 2025 |

I spent the weekend wrestling with auth, and it finally clicked (which feels super cool). Here’s the quick breakdown.

personal website project

The Setup

I’m using:

  • Next.js App Router
  • Server Actions
  • Supabase Auth
  • A simple AuthForm as the UI

My goal was straightforward: let users sign up and log in without setting up a separate backend server. Next.js + Supabase is perfect for this — as long as you understand which part runs where.

Blog Image


The “Aha” Moment: Three Layers Talking to Each Other

The architecture turns out to be really clean once you see the pattern:

  1. Client (React) — collects email and password
  2. Server Action — runs securely on the server, talks to Supabase
  3. Supabase — handles the actual auth and cookies

Everything else is just glue.


Client Side: The Auth Form

This is what the user sees. It’s a normal client component with a form and a button:

<form action={handleSubmit}>

When the form submits, I use startTransition() so the UI stays smooth while the server action runs:

startTransition(async () => {
  const email = formData.get(”email”) as string;
  const password = formData.get(”password”) as string;

  const result = await loginAction(email, password);
});

startTransition() was new to me, but I love what it does:
let React know this is not urgent UI work → no freezing, no jank.


Server Side: The Actual Login Logic

This is where Next.js feels like cheating — I don’t need Express, routes, or API endpoints. I just write a function:

“use server”;

export const loginAction = async (email, password) => {
  const { auth } = await createClient();
  const { error } = await auth.signInWithPassword({ email, password });
  if (error) throw error;
  return { errorMessage: null };
};

Anything inside “use server” runs on the backend automatically.
Supabase handles the heavy lifting — I just pass the credentials along and catch errors.


The Supabase Client (SSR)

I used Supabase’s SSR helper to create a server-side client that automatically syncs with cookies:

const client = createServerClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_ANON_KEY,
  {
    cookies: {
      getAll() { ... },
      setAll() { ... },
    },
  }
);

This is what makes login “stick.” Supabase updates the auth cookies, and my app can read them later with:

const user = await getUser();

No token juggling. No localStorage. Just clean SSR auth.


Middleware (Optional but Interesting)

I also explored Supabase + Next middleware, which lets me refresh sessions automatically on every request. Mine isn’t fully wired up yet, but it’s cool to know what’s possible:

  • Refresh auth cookies
  • Protect routes
  • Redirect logged-out users

It’s a nice bonus for when the app grows.


What I Learned (In Plain English)

  • Next isn’t “frontend-only.”
    Server Actions make it a full backend when you need it.
  • Supabase handles the entire auth flow.
    You rarely touch tokens directly — it all runs through cookies.
  • Client Components should stay simple.
    Collect input → send to server → react to the result.
  • Server Components and Server Actions are where the actual logic should live.
  • Once you get the structure, everything falls into place.

What’s Next

Now that basic auth works, I’m moving on to:

  • protecting pages with getUser()
  • wiring up a user dashboard
  • designing the first real flow of my app
  • writing clean /actions/* architecture as the app expands

It finally feels like the pieces are clicking.
And it’s fun to watch the project slowly turn into something real :)