Better Auth in Your App
Sign users up, in and out, read the session and protect a page in Next.js

Sprout presents
Sign up, sign in, sign out, read the session, protect a page. Five operations, each a few lines, all running inside your own app.Sign up, sign in, sign out, read the session, protect a page. Five operations, each a few lines, all running inside your own app.

Better Auth is installed and your tables exist. Now the fun part... actually USING it. You'll build the handful of pieces almost every app needs, all based on the examples in the official Better Auth docs.
The Two Halves#
Remember those two files from the last lesson? They show up in every single example below.
authClientfromlib/auth-client.tsruns in the browser. You use it in client components to sign up, sign in and sign out.authfromlib/auth.tsruns on the server. You use it in server components to check who's logged in.
Signing Up#
The docs use authClient.signUp.email to create a new account. Here it is inside a simple Next.js sign up page. The form and state are plain React I added for this lesson, and the Better Auth call itself matches the docs.
// app/sign-up/page.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { authClient } from "@/lib/auth-client";
export default function SignUpPage() {
const router = useRouter();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const { data, error } = await authClient.signUp.email(
{
email,
password,
name,
callbackURL: "/dashboard",
},
{
onSuccess: () => {
router.push("/dashboard");
},
onError: (ctx) => {
alert(ctx.error.message);
},
}
);
}
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
<input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
<button type="submit">Sign up</button>
</form>
);
}A few things worth knowing about this call, all straight from the docs.
email,passwordandnameare the user's details. The docs note the password needs at least 8 characters by default.callbackURLis where to send the user after they verify their email. It's optional.- The second object holds callbacks.
onSuccessis what actually sends the new user to/dashboardhere, sincecallbackURLonly kicks in after email verification. The docs also showonRequest, which is handy for a loading spinner.
Signing In#
Signing in looks almost identical, using authClient.signIn.email. Here's the call from the docs.
const { data, error } = await authClient.signIn.email({
email,
password,
callbackURL: "/dashboard",
rememberMe: false
}, {
//callbacks
})rememberMe controls whether the session sticks around after the browser closes. The docs say it defaults to true.
Here's that call dropped into a sign in page, built the same way as the sign up page. You'll need this page for the sign out button and the protected page below, since both send people to /sign-in.
// app/sign-in/page.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { authClient } from "@/lib/auth-client";
export default function SignInPage() {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
await authClient.signIn.email(
{ email, password },
{
onSuccess: () => {
router.push("/dashboard");
},
onError: (ctx) => {
alert(ctx.error.message);
},
}
);
}
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
<button type="submit">Sign in</button>
</form>
);
}Signing Out#
To log someone out, call authClient.signOut. The docs show passing fetchOptions so you can redirect once it's done. Here it is as a lil button that sends the user to /sign-in afterwards. That page has to exist FIRST, so make sure you built app/sign-in/page.tsx from the section above before you add this button. Otherwise the redirect lands on a 404 and you'll be scratching your head.
// components/sign-out-button.tsx
"use client";
import { useRouter } from "next/navigation";
import { authClient } from "@/lib/auth-client";
export function SignOutButton() {
const router = useRouter();
return (
<button
onClick={async () => {
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
router.push("/sign-in");
},
},
});
}}
>
Sign out
</button>
);
}Reading the Session#

A session is how your app remembers that someone's logged in. Better Auth lets you read it in the browser and on the server.
On the Client#
In a client component, use the useSession hook. The docs say it updates your UI right away when the session changes, like when someone signs out.
// components/user.tsx
"use client";
import { authClient } from "@/lib/auth-client";
export function User() {
const {
data: session,
isPending, // loading state
error, // error object
refetch, // refetch the session
} = authClient.useSession();
if (isPending) return <p>Loading...</p>;
if (!session) return <p>Not signed in</p>;
return <p>Signed in as {session.user.name}</p>;
}Not a hook person? The docs also show authClient.getSession(), which returns the same kind of data and error.
On the Server#
On the server, call auth.api.getSession and hand it the request headers. In Next.js you grab those from next/headers.
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
const session = await auth.api.getSession({
headers: await headers()
})If nobody's logged in, session comes back empty, so ALWAYS check it before you use it.
Protecting a Page#
Now let's put it all together. The docs recommend checking the session in each protected page or route. Here's their example for a dashboard page.
// app/dashboard/page.tsx
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth.api.getSession({
headers: await headers()
})
if(!session) {
redirect("/sign-in")
}
return <h1>Welcome {session.user.name}</h1>
}So what actually happens when someone visits /dashboard?
Step 1. The Page Asks for the Session#
The server component reads the request headers, which carry the login cookie, and asks Better Auth who this is.
Step 2. Better Auth Checks It#
Better Auth looks up the session. If the cookie is missing or isn't valid, there's no session.
Step 3. Visitors Get Redirected#
No session? Then redirect("/sign-in") sends the visitor off to your sign in page.
Step 4. Members See the Page#
With a valid session, the page renders and greets the user by name. Easy.
TL;DR#
- Use
authClientin client components andauth.apion the server. authClient.signUp.email,authClient.signIn.emailandauthClient.signOutcover sign up, sign in and sign out.- New users are signed in automatically after sign up unless you set
autoSignIntofalse. - Read the session with
authClient.useSession()in the browser andauth.api.getSessionwithawait headers()on the server. - Protect a page by checking the session and calling
redirectwhen it is missing.
What's Next?#
You've now got working sign up, sign in, sign out and a protected page (lil victory lap, you earned it). From here, the Better Auth docs show how to add social sign in like GitHub and plugins like two factor authentication...
This lesson ends with 2 short activities.
