Supabase sign-up page for Next.js

An animated create-account card on supabase.auth.signUp that handles the two cases most tutorials skip: an email that already has an account, and a sign-up that waits on email confirmation.

Gateway Dive, preview
One payment$3

By buying you agree to the Terms.

Ready forCursorv0Lovableshadcn CLI

Full product pageReact · Tailwind · TypeScript

Use it in Next.js

app/signup/page.tsx

"use client";
import { useRef } from "react";
import { useRouter } from "next/navigation";
import GatewayDive from "@/components/gateway-dive/GatewayDive";
import { createClient } from "@/lib/supabase/client";

export default function SignUp() {
  const router = useRouter();
  const supabase = createClient();
  const needsConfirm = useRef(false);

  return (
    <main style={{ height: "100dvh" }}>
      <GatewayDive
        autoplayDemo={false}
        defaultMode="signup"
        diveOnSuccess={false}
        brand="Acme"
        onSubmit={async ({ mode, name, email, password }) => {
          if (mode === "signin") {
            const { error } = await supabase.auth.signInWithPassword({ email, password });
            needsConfirm.current = false;
            return error ? { ok: false, error: error.message } : { ok: true };
          }
          const { data, error } = await supabase.auth.signUp({
            email,
            password,
            options: { data: { name }, emailRedirectTo: `${window.location.origin}/auth/callback?next=/dashboard` },
          });
          if (error) return { ok: false, error: error.message };
          // With confirmations on, an existing email comes back as a user with no identities and no error.
          if (data.user && data.user.identities?.length === 0) {
            return { ok: false, error: "That email already has an account. Sign in instead." };
          }
          needsConfirm.current = !data.session;
          return { ok: true };
        }}
        onSuccess={({ email }) =>
          router.push(needsConfirm.current ? `/check-email?to=${encodeURIComponent(email)}` : "/dashboard")
        }
      />
    </main>
  );
}

app/auth/callback/route.ts

import { NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/server";

// The confirmation link lands here with ?code=. Same route as the Google login recipe.
export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url);
  const code = searchParams.get("code");
  const next = searchParams.get("next") ?? "/";
  const safeNext = next.startsWith("/") && !next.startsWith("//") ? next : "/";

  if (code) {
    const supabase = await createClient();
    const { error } = await supabase.auth.exchangeCodeForSession(code);
    if (!error) return NextResponse.redirect(`${origin}${safeNext}`);
  }
  return NextResponse.redirect(`${origin}/login?error=confirm`);
}

Good to know

  • With Confirm email on (the Supabase default), signUp returns a user but no session. The snippet sends those people to a check-your-email page instead of the dashboard. Turn confirmations off and data.session is set, so they go straight in.
  • Supabase does not return an error for an email that is already registered when confirmations are on. It returns a user with an empty identities list, so the snippet checks for that and shakes the card with a sign-in hint.
  • diveOnSuccess is off here because a new account that still needs confirming shouldn't dive into the app. The button turns into a check and onSuccess routes.
  • The confirmation link uses the PKCE flow, so it has to be opened in the same browser that signed up. Someone who opens it on their phone lands on the error path; tell them that on the check-email page.
  • lib/supabase/client.ts and lib/supabase/server.ts are the same two files as on the Supabase Google login page.

Install

  1. Copy from the zipPut react/src/components into src/, then npm i @supabase/supabase-js @supabase/ssr.
  2. Or ask your AIPaste PROMPT.md into Cursor, Lovable or v0.

Props

defaultMode
'signup' opens on create account
onSubmit
receives mode, name, email, password; returns { ok, error? }
onSuccess
runs when the check appears
diveOnSuccess
false to route yourself instead of diving
providers / onProvider
social buttons on the same card
autoplayDemo
false in a real app

How qodesign compares

All comparisons, with prices read on each library's own site.

Questions

Why doesn't signUp fail for an email that already exists?

With email confirmations on, Supabase hides whether an address is registered. You get a user object with identities set to an empty array, which is what the snippet checks.

Where does the name field go?

Into options.data, which Supabase stores as user_metadata on the new user. Read it back from user.user_metadata.name.

Can people switch to sign in on the same card?

Yes. The card morphs between create account and sign in, and onSubmit gets the mode, so one handler covers both.

Does the sign-up page work in Next.js?

Yes, as a client component. Keep "use client" at the top of the file that renders it, as in the snippet above. App Router and Pages Router both work.

Can Cursor or v0 add it for me?

Yes. Each download has a PROMPT.md. Paste it into Cursor, Lovable, v0 or Claude Code and it fits the piece into your app.

Can I use it in client work?

Yes, in personal and client projects. You can't resell the code itself as a kit.