---
title: "Sign Up and Sign In Pages"
description: "Build sign-up and sign-in pages using route groups, Server Actions, and proper error handling with Supabase Auth."
canonical_url: "https://vercel.com/academy/subscription-store/sign-up-and-sign-in-pages"
md_url: "https://vercel.com/academy/subscription-store/sign-up-and-sign-in-pages.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-11T07:45:06.663Z"
content_type: "lesson"
course: "subscription-store"
course_title: "Launch a Subscription Store with Vercel and Stripe"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Sign Up and Sign In Pages

# 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:**
   ```bash
   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:**
   - Visit <http://localhost:3000/sign-in>
   - Enter the credentials you created
   - Click Sign in
   - You should be redirected to `/protected`

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

```bash
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:

```typescript title="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`:

```typescript title="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`:

```typescript title="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`:

```typescript title="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`:

```typescript title="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:

```typescript
const searchParams = await props.searchParams;
```


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
