You'll learn how to implement authentication with Better Auth in a client - server architecture, where the frontend is separate from the backend.
The backend uses Hono and Bun, whereas the frontend uses React with Vite. For brevity, this article will only include the relevant bits instead of taking you through the whole process of building the full-stack application from scratch.
You can find the link to the repository with the complete code at the end of this article.
Backend: Better Auth with Hono and Bun
We'll start by installing Better Auth:
bun add better-auth
After that, create an .env
file with the following environment variables:
# Auth
BETTER_AUTH_SECRET=<generate-a-secret-key>
BETTER_AUTH_URL=<url-of-your-server> (e.g. http://localhost:1234)
Now you're ready to configure Better Auth.
Create the Better Auth Instance
In my case, I placed the Better Auth instance in the auth.ts file. My project has the following requirements:
- it uses PostgreSQL for the database
- it uses Prisma ORM
- the app should allow users to log in/sign up with email and password
- the app should allow users to log in/sign up with their Google or GitHub accounts
- users can have different roles
import { Role } from "@prisma/client";
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import prisma from "@/db/index";
import env from "@/env";
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "postgresql",
}),
// Allow requests from the frontend development server
trustedOrigins: ["http://localhost:5173"],
emailAndPassword: {
enabled: true,
},
socialProviders: {
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
},
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
},
},
user: {
additionalFields: {
roles: {
type: [Role.STUDENT, Role.CREATOR, Role.ADMIN],
required: true,
defaultValue: [Role.STUDENT],
},
},
},
});
The above code does the following:
- It sets up the database to use Prisma ORM and PostgreSQL.
- It specifies the trusted origins, which are the origins allowed to make requests.
- In this case, it's the
client
(frontend). All the other origins that are not specified are automatically blocked.
- In this case, it's the
- It enables email/password authentication and configures social login providers.
- Lastly, it extends the core
user
schema to add the additional fieldroles
- That means that each user can have multiple roles.
Once you're done with configuring your Better Auth instance, run the following command to generate the ORM schema:
bunx @better-auth/cli generate
It adds all the required models, fields, and relationships to the Prisma schema file.
Create the API Handler
The next step is to set up a route handler for the auth API requests.
The route below uses the handler provided by Better Auth to serve all POST
and GET
requests to the /api/auth
endpoint.
import { auth } from "@/lib/auth";
import { createRouter } from "@/lib/create-app";
const router = createRouter();
router.on(["POST", "GET"], "/auth/**", (c) => {
return auth.handler(c.req.raw);
});
export default router;
The above code lives in the routes/auth.ts file.
Lastly, you need to mount the route in the app.ts file.
....
import createApp from "@/lib/create-app";
import auth from "@/routes/auth";
....
const app = createApp();
const routes = [userSettings, creator, student, courses, auth] as const;
routes.forEach((route) => {
app.basePath("/api").route("/", route);
});
....
export default app;
Once that's done, Better Auth on the backend is all set.
Frontend: Better Auth with React and Vite
The first step is to install Better Auth in the client directory:
bun add better-auth
Once that's installed, navigate to /src/lib/auth-client and add the following code:
import { createAuthClient } from "better-auth/react";
import { inferAdditionalFields } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [
inferAdditionalFields({
user: {
roles: {
type: "string[]",
},
},
}),
],
});
export const {
useSession,
signIn,
signUp,
signOut,
forgetPassword,
resetPassword,
} = authClient;
export type Session = typeof authClient.$Infer.Session;
export type User = typeof authClient.$Infer.Session.user;
The client-side library allows you to interact with the Better Auth instance from the server.
The above code:
- Calls the
createAuthClient
function to create the auth client. - Stores the result of the function call in a constant
authClient
.- It returns various hooks and functions related to authentication.
- Uses the
plugins
property to extend the user object by adding theroles
property. - Exports specific auth hooks and methods, so you don't have to import the entire
authClient
object to use them.- You can import and use them directly.
- Lastly, it uses the
$Infer
property from Better Auth to export the "User" and "Sessions" types.
You're done and can start authenticating users.
Register Component
You need to import the signUp
object from the /src/lib/auth-client file and pass the data provided by the user to its email
method.
The method provides various callbacks, such as onRequest
, onSuccess
, and onError
, allowing you to perform various actions at different stages of the registration process.
For example, the onRequest
callback could be used to set a loading state until the registration resolves.
In this example, the user is redirected to the homepage if the registration is successful (redundant since that's the default Better-Auth behaviour, but its purpose is to showcase the onSuccess callback).
If there is an error during the registration process, it'll log the error (not recommended for production use).
...
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (password !== confirmPassword) {
alert("Passwords do not match");
return;
}
await signUp.email(
{ email, password, name, roles: ["STUDENT"] },
{
onSuccess: () => {
navigate({ to: "/" });
},
onError: (error) => {
console.error(error);
},
}
);
};
...
Better Auth automatically redirects the user to the home page on successful registration. If you want to change the default behaviour, see how to do it here.
Login Component
The login functionality works similarly to the registration functionality.
Import the signIn
object from the auth-client and call the appropriate authentication methods. In this case:
email
to allow the users to sign in with email and passwordsocial
to allow the users to sign in via Google or GitHub
...
const handleEmailLogin = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
await signIn.email(
{ email, password },
{
onSuccess: () => {
navigate({ to: "/" });
},
onError: (error) => {
console.error(error);
},
}
);
};
const handleGoogleLogin = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
await signIn.social(
{ provider: "google" },
{
onSuccess: () => {
navigate({ to: "/" });
},
onError: (error) => {
console.error(error);
},
}
);
};
const handleGithubLogin = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
await signIn.social(
{ provider: "github" },
{
onSuccess: () => {
navigate({ to: "/" });
},
onError: (error) => {
console.error(error);
},
}
);
};
...
Demo
The video below shows the following flows:
- registers a user
- signs out the user
- signs in the user
The complete code is available in this repository.
What's next?
- Implement Resend to send emails
- Implement Polar to accept payments