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.tsis 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 usehooks.tsinstead. -
service.tsis 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/emailcodefor a session, and the privileged Admin API call that deletes a user. Nothing outside this module should importservice.tsdirectly —actions.tsandroutes/callback.route.tsare its only callers. -
actions.tsis the public server-side API — a"use server"file that the rest of the app imports instead ofservice.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.tswraps everyclient.tsfunction in a small React state machine (isLoading/error, plusisSentwhere 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.tsrefreshes 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/loginif there's no authenticated user, otherwise returns theAppUser.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
/dashboardrequires a signed-in user, usingrequireAuth()/requireRole()as the single entry point for access control. - An admin area or multi-tier product (free/pro/admin) by extending the
Roleunion andROLE_PERMISSIONSmap intypes.ts— the guard functions andhasPermission()already read from that single source. - A self-service account settings page (change password, resend
verification, delete account) entirely from
hooks.tsandactions.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.