Skip to content

Join the Seedly owners community →

Authentication

Better Auth in Your App

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

Written by 12 min read2 activities
Sprout, your presenter

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.

Sprout stands by a wooden revolving door with an open guest book and a coat rack
Sign up, sign in and sign out all run in your 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.

  • authClient from lib/auth-client.ts runs in the browser. You use it in client components to sign up, sign in and sign out.
  • auth from lib/auth.ts runs 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, password and name are the user's details. The docs note the password needs at least 8 characters by default.
  • callbackURL is where to send the user after they verify their email. It's optional.
  • The second object holds callbacks. onSuccess is what actually sends the new user to /dashboard here, since callbackURL only kicks in after email verification. The docs also show onRequest, 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#

Sprout checks a cookie shaped token with a magnifying glass against an open ledger
Check the real session, not just that a cookie exists

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 authClient in client components and auth.api on the server.
  • authClient.signUp.email, authClient.signIn.email and authClient.signOut cover sign up, sign in and sign out.
  • New users are signed in automatically after sign up unless you set autoSignIn to false.
  • Read the session with authClient.useSession() in the browser and auth.api.getSession with await headers() on the server.
  • Protect a page by checking the session and calling redirect when 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.