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
- Implement
app/actions.ts- fill in the TODO stubs forsignUpAction,signInAction, andsignOutAction - The
utils/redirect.tsutility and auth pages are already in the starter - Test sign-up and sign-in flows
Hands-on Exercise 1.4
Implement the Server Actions (TODO stubs are provided):
Requirements:
- Implement the
signInActionto authenticate users with Supabase - Implement the
signUpActionto create new accounts - Implement the
signOutActionto log users out - The sign-up and sign-in pages are already wired to call these actions
- Handle errors with
encodedRedirectfor user feedback
Implementation hints:
- The starter includes
AuthSubmitButtonandFormMessagecomponents - 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 statecomponents/form-message.tsx- Displays success/error messagescomponents/content.tsx- Page content wrappercomponents/ui/input.tsx- Styled form inputcomponents/ui/label.tsx- Styled form label
Try It
-
Start the dev server:
pnpm dev -
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
-
Test sign-in flow:
- Visit http://localhost:3000/sign-in
- Enter the credentials you created
- Click Sign in
- You should be redirected to
/protected
-
Test error handling:
- Try signing in with wrong credentials
- You should see: "Invalid login credentials"
-
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.tscontainssignUpAction,signInAction,signOutActionutils/redirect.tsexportsencodedRedirectfunction- 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:
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:
"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:
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:
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:
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:
- Supabase dashboard > Authentication > Providers
- 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;Was this helpful?