Logo FormaUI

FormaUI

All providers

Supabase Auth

Copy-paste authentication module for Next.js App Router projects using Supabase as the identity provider. Covers email/password sign up and sign in, OAuth (Google, GitHub, Facebook), password recovery, email verification, automatic session refresh, and role/permission-based route protection — no UI included, logic only.

Dependencies

  • @supabase/ssr: creates the browser and server Supabase clients and handles reading/writing auth cookies the way Next.js App Router expects.
  • @supabase/supabase-js: the underlying Supabase client, also used directly for the privileged Admin API client (account deletion).

Folder structure

auth/supabase/
├── types.ts                    // Shared types: AppUser, AppSession, Role, Permission, AuthError, AuthResult — plus hasPermission()
├── client.ts                   // Browser-only Supabase client + auth functions, for Client Components
├── service.ts                  // INTERNAL server-only Supabase client + raw auth operations — never imported directly by app code
├── actions.ts                  // "use server" — public server-side API (requireAuth, requireRole, getAuthUser, getAuthSession, deleteAccount)
├── middleware.ts                // Self-contained session refresh + route protection — copy as-is to the project root
├── routes/
│   └── callback.route.ts        // Copy as-is to app/api/auth/callback/route.ts — handles OAuth + email confirmation + password recovery redirects
├── hooks.ts                      // Client-side React hooks wrapping client.ts with loading/error state
└── setup.md                       // Usage-only docs: import map + examples

How the files work together

  • client.ts is the browser-side public API. Every function that has to run in the browser — sign up, sign in, sign out, OAuth redirect, password reset request, password update, email verification resend, reading the current user/session client-side — lives here. Client Components can import it directly, though most consumers will use hooks.ts instead.

  • service.ts is internal. It holds the server-side Supabase client and the raw operations that need trusted server context: reading the authenticated user with token revalidation, exchanging an OAuth/email code for a session, and the privileged Admin API call that deletes a user. Nothing outside this module should import service.ts directly — actions.ts and routes/callback.route.ts are its only callers.

  • actions.ts is the public server-side API — a "use server" file that the rest of the app imports instead of service.ts. It wraps the raw operations with the guard/authorization behavior a real app needs: redirecting unauthenticated or under-privileged requests, and exposing a safe, self-contained Server Action for account deletion.

  • hooks.ts wraps every client.ts function in a small React state machine (isLoading/error, plus isSent where relevant) so Client Components don't need to manage that state by hand. useUser() is the one reactive hook — it subscribes to Supabase's auth state changes so the current user stays in sync across tabs and after token refresh.

  • middleware.ts refreshes the session cookie on every request and redirects based on a public/protected route classification defined inline in the same file — it has no imports from the rest of the module, so it can be copied straight to the project root with nothing to adjust.

What actions.ts covers

  • requireAuth() — hard guard for a protected Server Component or Server Action; redirects to /login if there's no authenticated user, otherwise returns the AppUser.
  • requireRole(role) — same as above, plus redirects if the user's role doesn't match.
  • getAuthUser() / getAuthSession() — non-redirecting reads, for pages that behave differently for signed-in vs anonymous visitors instead of requiring auth outright.
  • deleteAccount() — self-service account deletion as a Server Action, wireable directly to a <form action={deleteAccount}> with no API route in between.

Sign up, sign in, sign out, OAuth, password reset, and email verification are not in actions.ts — they're browser operations against Supabase's client SDK and live in client.ts/hooks.ts instead, which is the pattern Supabase's own docs recommend for App Router.

Usage examples

Protect a Server Component page:

import { requireAuth } from "@/auth/supabase/actions";

export default async function DashboardPage() {
  const user = await requireAuth();
  return <div>Welcome, {user.email}</div>;
}

Restrict a page to a specific role:

import { requireRole } from "@/auth/supabase/actions";

export default async function AdminPage() {
  const user = await requireRole("admin");
  return <div>Admin panel for {user.email}</div>;
}

Sign up (Client Component):

"use client";
import { useSignUp } from "@/auth/supabase/hooks";

export function SignUpForm() {
  const { signUp, isLoading, error } = useSignUp();

  async function handleSubmit(email: string, password: string) {
    const result = await signUp(email, password);
    if (result.success) {
      // show a "check your email" state
    }
  }

  return null; // your form JSX
}

Sign in with OAuth (Client Component):

"use client";
import { useOAuthSignIn } from "@/auth/supabase/hooks";

export function GoogleSignInButton() {
  const { signIn, isLoading } = useOAuthSignIn();
  return (
    <button onClick={() => signIn("google")} disabled={isLoading}>
      Continue with Google
    </button>
  );
}

Read the current user reactively (Client Component):

"use client";
import { useUser } from "@/auth/supabase/hooks";

export function UserBadge() {
  const { user, isLoading } = useUser();
  if (isLoading) return null;
  return user ? <span>{user.email}</span> : null;
}

Delete the current user's account:

import { deleteAccount } from "@/auth/supabase/actions";

export function DeleteAccountButton() {
  return (
    <form action={deleteAccount}>
      <button type="submit">Delete my account</button>
    </form>
  );
}

What you can build with this

This module is a complete auth layer, not a starter kit for one flow — it covers the full lifecycle from sign up through account deletion, plus the route-protection scaffolding most apps need on day one. On top of it you can build:

  • A gated dashboard or SaaS product where every route under /dashboard requires a signed-in user, using requireAuth()/requireRole() as the single entry point for access control.
  • An admin area or multi-tier product (free/pro/admin) by extending the Role union and ROLE_PERMISSIONS map in types.ts — the guard functions and hasPermission() already read from that single source.
  • A self-service account settings page (change password, resend verification, delete account) entirely from hooks.ts and actions.ts, with no additional API routes to write.
  • Any app that needs social login without owning OAuth token handling — Google, GitHub, and Facebook are wired end-to-end through signInWithOAuth() and the callback route.