Vercel Logo

Sign Up and Sign In Pages

Authentication forms are the gateway to your app. The starter includes pre-built UI components - you'll wire them up with Server Actions to handle sign-up and sign-in securely on the server.

Outcome

Create working sign-up and sign-in pages that authenticate users with Supabase using Server Actions.

Fast Track

  1. Implement app/actions.ts - fill in the TODO stubs for signUpAction, signInAction, and signOutAction
  2. The utils/redirect.ts utility and auth pages are already in the starter
  3. Test sign-up and sign-in flows

Hands-on Exercise 1.4

Implement the Server Actions (TODO stubs are provided):

Requirements:

  1. Implement the signInAction to authenticate users with Supabase
  2. Implement the signUpAction to create new accounts
  3. Implement the signOutAction to log users out
  4. The sign-up and sign-in pages are already wired to call these actions
  5. Handle errors with encodedRedirect for user feedback

Implementation hints:

  • The starter includes AuthSubmitButton and FormMessage components - use them
  • Server Actions must have "use server" at the top of the file
  • Use a route group (auth) to share layout between auth pages
  • Pass error messages through URL search params with encodedRedirect

Components available in starter:

  • components/auth-submit-button.tsx - Submit button with loading state
  • components/form-message.tsx - Displays success/error messages
  • components/content.tsx - Page content wrapper
  • components/ui/input.tsx - Styled form input
  • components/ui/label.tsx - Styled form label

Try It

  1. Start the dev server:

    pnpm dev
  2. Test sign-up flow:

    • Visit http://localhost:3000/sign-up
    • Enter an email and password (min 6 characters)
    • Click Sign in (button shows loading state)
    • You should be redirected to /protected
  3. Test sign-in flow:

  4. Test error handling:

    • Try signing in with wrong credentials
    • You should see: "Invalid login credentials"
  5. Verify in Supabase:

    • Go to Supabase dashboard > Authentication > Users
    • Your test user should appear

Commit

git add -A
git commit -m "feat(auth): add sign-up and sign-in pages with Server Actions"

Done-When

  • app/actions.ts contains signUpAction, signInAction, signOutAction
  • utils/redirect.ts exports encodedRedirect function
  • Sign-up page renders at /sign-up
  • Sign-in page renders at /sign-in
  • Forms show loading state while submitting
  • Users can create accounts
  • Users can sign in
  • Error messages display for invalid credentials

Solution

Step 1: Verify Redirect Utility

The utils/redirect.ts utility is already in the starter:

utils/redirect.ts
import { redirect } from "next/navigation";
 
/**
 * Redirects to a specified path with an encoded message as a query parameter.
 */
export function encodedRedirect(
  type: "error" | "success",
  path: string,
  message: string,
) {
  return redirect(`${path}?${type}=${encodeURIComponent(message)}`);
}

This utility encodes error/success messages in the URL so they survive the redirect.

Step 2: Implement Server Actions

Replace the TODO stubs in app/actions.ts:

app/actions.ts
"use server";
 
import { createSupabaseClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
import { encodedRedirect } from "@/utils/redirect";
 
export const signUpAction = async (formData: FormData) => {
  const email = formData.get("email") as string;
  const password = formData.get("password") as string;
  const client = await createSupabaseClient();
 
  const url = process.env.VERCEL_URL
    ? `https://${process.env.VERCEL_URL}/protected`
    : "http://localhost:3000/protected";
 
  const { error } = await client.auth.signUp({
    email,
    password,
    options: {
      emailRedirectTo: url,
    },
  });
 
  if (error) {
    return encodedRedirect("error", "/sign-up", error.message);
  }
 
  return redirect("/protected");
};
 
export const signInAction = async (formData: FormData) => {
  const email = formData.get("email") as string;
  const password = formData.get("password") as string;
  const client = await createSupabaseClient();
 
  const { error } = await client.auth.signInWithPassword({
    email,
    password,
  });
 
  if (error) {
    return encodedRedirect("error", "/sign-in", error.message);
  }
 
  return redirect("/protected");
};
 
export const signOutAction = async () => {
  const client = await createSupabaseClient();
  await client.auth.signOut();
  return redirect("/sign-in");
};

Step 3: Verify Auth Layout

The auth layout is already in the starter at app/(auth)/layout.tsx:

app/(auth)/layout.tsx
import Content from "@/components/content";
 
export default function AuthLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <Content>{children}</Content>;
}

The route group (auth) shares this layout without adding "auth" to the URL.

Step 4: Verify Sign-Up Page

The sign-up page is already in the starter at app/(auth)/sign-up/page.tsx:

app/(auth)/sign-up/page.tsx
import { signUpAction } from "@/app/actions";
import AuthSubmitButton from "@/components/auth-submit-button";
import { FormMessage, Message } from "@/components/form-message";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Link from "next/link";
 
export default async function SignUp(props: {
  searchParams: Promise<Message>;
}) {
  const searchParams = await props.searchParams;
 
  return (
    <form
      className="flex-1 flex flex-col w-full max-w-sm mx-auto mt-24"
      action={signUpAction}
    >
      <h1 className="text-2xl font-medium">Join the Guild</h1>
      <p className="text-sm text-foreground">
        Already a member?{" "}
        <Link className="text-foreground font-medium underline" href="/sign-in">
          Sign in
        </Link>
      </p>
      <div className="flex flex-col gap-2 [&>input]:mb-3 mt-8">
        <Label htmlFor="email">Email</Label>
        <Input name="email" placeholder="forager@example.com" required />
        <Label htmlFor="password">Password</Label>
        <Input
          type="password"
          name="password"
          placeholder="Create a password"
          required
        />
        <AuthSubmitButton />
        <FormMessage message={searchParams} />
      </div>
    </form>
  );
}

Step 5: Verify Sign-In Page

The sign-in page is already in the starter at app/(auth)/sign-in/page.tsx:

app/(auth)/sign-in/page.tsx
import { signInAction } from "@/app/actions";
import AuthSubmitButton from "@/components/auth-submit-button";
import { FormMessage, Message } from "@/components/form-message";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Link from "next/link";
 
export default async function SignIn(props: {
  searchParams: Promise<Message>;
}) {
  const searchParams = await props.searchParams;
 
  return (
    <form
      className="flex-1 flex flex-col w-full max-w-sm mx-auto mt-24"
      action={signInAction}
    >
      <h1 className="text-2xl font-medium">Welcome Back, Forager</h1>
      <p className="text-sm text-foreground">
        Not a member yet?{" "}
        <Link className="text-foreground font-medium underline" href="/sign-up">
          Join the Guild
        </Link>
      </p>
      <div className="flex flex-col gap-2 [&>input]:mb-3 mt-8">
        <Label htmlFor="email">Email</Label>
        <Input name="email" placeholder="forager@example.com" required />
        <Label htmlFor="password">Password</Label>
        <Input
          type="password"
          name="password"
          placeholder="Your password"
          required
        />
        <AuthSubmitButton />
        <FormMessage message={searchParams} />
      </div>
    </form>
  );
}

File Structure After This Lesson

app/
├── (auth)/
│   ├── layout.tsx        ← Already in starter
│   ├── sign-up/
│   │   └── page.tsx      ← Already in starter, calls signUpAction
│   └── sign-in/
│       └── page.tsx      ← Already in starter, calls signInAction
├── actions.ts            ← Implemented in this lesson
└── ...
utils/
├── redirect.ts           ← Already in starter
└── supabase/
    ├── client.ts         ← Implemented in lesson 1.3
    └── server.ts         ← Implemented in lesson 1.3

How Server Actions Work

User submits form
    ↓
Browser POSTs to current URL
    ↓
Next.js routes to Server Action
    ↓
Action runs on server (secure)
    ↓
Supabase authenticates user
    ↓
Action calls redirect()
    ↓
Browser navigates to new page

The "use server" directive ensures the code never runs in the browser. Form data is sent securely to the server.

Troubleshooting

"Invalid login credentials" on sign-up:

Supabase may require email confirmation. To disable for testing:

  1. Supabase dashboard > Authentication > Providers
  2. Under Email, toggle "Confirm email" off

Form submits but page doesn't redirect:

The proxy (next lesson) handles the redirect properly. For now, manually navigate to /protected after sign-in.

"Cannot read properties of undefined" error:

Make sure searchParams is awaited - it's a Promise in Next.js 16:

const searchParams = await props.searchParams;