Authenticating users in Astro with Better Auth: A Step-by-Step Guide

Black Friday: Enjoy a 25% discount on the starter kits. Use code BLACKFRIDAY2024 at checkout.

LaunchFast Logo LaunchFast

Authenticating users in Astro with Better Auth: A Step-by-Step Guide

Rishi Raj Jain
Authenticating users in Astro with Better Auth

In this guide, you will learn how to authenticate users in an Astro application using Better Auth. You will go through the process of setting up a new Astro project, enabling server-side rendering using Vercel adapter, and integrating Better Auth to authenticate users seamlessly.

Sponsored AICardify - AI-generated greeting cards

Generate AI Powered Greeting Cards For Every Occasion

Prerequisites

You’ll need the following:

Table Of Contents

Create a new Astro application

Let’s get started by creating a new Astro project. Execute the following command:

Terminal window
npm create astro@latest my-app

npm create astro is the recommended way to scaffold an Astro project quickly.

When prompted, choose:

  • Empty when prompted on how to start the new project.
  • Yes when prompted if plan to write Typescript.
  • Strict when prompted how strict Typescript should be.
  • Yes when prompted to install dependencies.
  • Yes when prompted to initialize a git repository.

Once that’s done, you can move into the project directory and start the app:

Terminal window
cd my-app
npm run dev

The app should be running on localhost:4321.

Now, let’s move on to enabling server-side rendering in your Astro application.

Integrate Vercel adapter in your Astro project

To authenticate and maintain user sessions with Better Auth, you will enable server-side rendering in your Astro project via the Vercel adapter. Execute the following command:

Terminal window
npx astro add vercel

When prompted, choose the following:

  • Yes when prompted whether to install the Vercel dependencies.
  • Yes when prompted whether to make changes to Astro configuration file.

You have succesfully enabled server-side rendering in Astro.

Now, let’s move to spinning up a serverless Postgres as the database.

Create a Neon (Serverless Postgres) project

If you do not have one already, create a Neon (Serverless Postgres) project. Save your connection details including your password. Here is how you’d do it:

  1. Navigate to the Projects page in the Neon Console.
  2. Click New Project.
  3. Specify your project settings and click Create Project.
  4. Create a .env file in the root directory of your Astro project, and define a DATABASE_URL key with the connection string obtained as the value.

Now, let’s move to setting up Better Auth in the Astro project.

Integrate Better Auth in your Astro project

Install relevant dependencies

First, execute the command below to install the necessary packages for building authentication with Better Auth:

Terminal window
npm install better-auth pg
npm install -D @types/pg dotenv-cli

The commands above install the following:

  • better-auth: A framework-agnostic authentication (and authorization) framework for TypeScript.
  • pg: A PostgreSQL client for Node.js.
  • @types/pg: Type definitions for the pg package.
  • dotenv-cli: A package to load dotenv files.

Now, let’s move to set up type definitions for the user and session object in your Astro project.

Set up Type Definitions

Next, set up type definitions for user and session objects in the src/env.d.ts file:

src/env.d.ts
/// <reference path="../.astro/types.d.ts" />
declare namespace App {
interface Locals {
user: import('better-auth').User | null
session: import('better-auth').Session | null
}
}

Now, let’s move to configuring universal access to environment variables.

Load environment variables

Update your package.json to always use the dotenv to load the environment variables so that they are accessible via both import.meta.env and process.env objects:

package.json
{
"type": "module",
"scripts": {
"dev": "dotenv -- astro dev",
"build": "dotenv -- astro build",
"preview": "dotenv -- astro preview"
},
"dependencies": {
"@astrojs/vercel": "^7.8.2",
"astro": "^4.16.13",
"better-auth": "^1.0.0",
"pg": "^8.13.1",
"typescript": "^5.7.2"
},
"devDependencies": {
"@types/pg": "^8.11.10",
"dotenv-cli": "^7.4.4"
}
}

Finally, update the .env file to have a unique random value for BETTER_AUTH_SECRET key.

.env
BETTER_AUTH_SECRET="a-32-character-secret"

Now, let’s move to creating utility files to access Better Auth in your Astro application.

Create helper files for using Better Auth

You will be creating two utility files, src/auth.ts and src/auth-client.ts to access Better Auth on the server side and client side, respectively.

In src/auth.ts (code below), you are going to use a Pool to connect to your Postgres instance to persist the sessions and user object in the database, and enable email and password authentication.

src/auth.ts
import pkg from 'pg'
import { betterAuth } from 'better-auth'
const { Pool } = pkg
export const auth = betterAuth({
emailAndPassword: { enabled: true },
database: new Pool({ connectionString: process.env.DATABASE_URL })
})

In the src/auth-client.ts (code below), you are instantiating a new better-auth instance to be used in the client side interactions.

src/auth-client.ts
import { createAuthClient } from 'better-auth/client'
export const authClient = createAuthClient()

Create relevant schema in the database

To create the schema per your configuration defined in src/auth.ts file in your database automatically, execute the command below:

Terminal window
npx @better-auth/cli migrate

Now, let’s move to defining an API route to allow you to authenticate users.

Define a catch-all authentication API Route

Better Auth does the heavy lifting of creating and managing the logic of validating credentials, creating (or updating) the user and relevant session objects in the database. You just need to create a catch-all api route in your Astro project as follows in the src/pages/api/auth/[...all].ts file:

src/pages/api/auth/[...all].ts
import { auth } from '@/auth'
import type { APIRoute } from 'astro'
export const ALL: APIRoute = async (ctx) => {
return auth.handler(ctx.request)
}

Now, let’s move to creating a middleware that dynamically updates the user and session object to be accessed globally.

Intercept all incoming requests using Astro middleware

To make sure that each request maintains a user and session information is accessible over the server-side endpoints, and in .astro pages during server-side rendering, you are going create a middleware that uses Better Auth to decode/encode a user session from the cookie.

Create a file middleware.ts in the src directory with the following code:

src/middleware.ts
import { auth } from '@/auth'
import { defineMiddleware } from 'astro:middleware'
export const onRequest = defineMiddleware(async (context, next) => {
context.locals.user = null
context.locals.session = null
const isAuthed = await auth.api.getSession({
headers: context.request.headers,
})
if (isAuthed) {
context.locals.user = isAuthed.user
context.locals.session = isAuthed.session
}
return next()
})

The code makes sure to mark both user and session as null by default, and assign the respective values obtained from the database using the relevant cookies from the request. This allows you to always know the correct state of user authentication.

Now, let’s move on to creating the index route of your Astro application.

Create the index route

Create a index.astro file inside the src/pages directory with the following code:

---
if (!Astro.locals.user?.id) return Astro.redirect('/signin')
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
</head>
<body>
{JSON.stringify(Astro.locals.user)}
<button id="signOutButton">Sign Out</button>
<script>
import { authClient } from '@/auth-client'
document.getElementById('signOutButton')?.addEventListener('click', async () => {
await authClient.signOut()
window.location.href = '/signin'
})
</script>
</body>
</html>

The code above does the following:

  • Redirects user to the sign in page if the user is not authenticated.
  • Shows the information stored in the user object pertaining to the authenticated user.
  • Shows a sign out button that upon click invokes Better Auth’s signOut utility to sign out the user.

Now, let’s move to create a sign in route in your Astro application.

Create a Sign in page

Create a signin.astro file inside the src/pages directory with the following code:

---
if (Astro.locals.user?.id) return Astro.redirect('/')
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
</head>
<body>
<form id="signin-form">
<input type="email" name="email" placeholder="Email" required />
<input required type="password" name="password" placeholder="Password" />
<button type="submit">Sign In</button>
</form>
<p>Don't have an account? <a href="/signup">Sign up here</a>.</p>
<script>
import { authClient } from '@/auth-client'
document.getElementById('signin-form')?.addEventListener('submit', async (event) => {
event.preventDefault()
const email = event.target?.email.value
const password = event.target?.password.value
const tmp = await authClient.signIn.email({
email,
password,
})
if (Boolean(tmp.error) === false) window.location.href = '/'
})
</script>
</body>
</html>

The code above does the following:

  • Redirects user to the index page if the user is already authenticated.
  • Creates a sign in form and attaches the submit event listener.
  • Attempts to sign in the user using the email and password provided.

Now, let’s move to create a sign up route in your Astro application.

Create a Sign up page

Create a signup.astro file inside the src/pages directory with the following code:

---
if (Astro.locals.user?.id) return Astro.redirect('/')
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
</head>
<body>
<form id="signup-form">
<input type="text" name="name" placeholder="Name" required />
<input type="email" name="email" placeholder="Email" required />
<input required type="password" name="password" placeholder="Password" />
<button type="submit">Sign up</button>
</form>
<p>Already have an account? <a href="/signin">Sign in here</a>.</p>
<script>
import { authClient } from '@/auth-client'
document.getElementById('signup-form')?.addEventListener('submit', async (event) => {
event.preventDefault()
const name = event.target?.name.value
const email = event.target?.email.value
const password = event.target?.password.value
const tmp = await authClient.signUp.email({
name,
email,
password,
})
if (Boolean(tmp.error) === false) window.location.href = '/'
})
</script>
</body>
</html>

The code above does the following:

  • Redirects user to the index page if the user is already authenticated.
  • Creates a sign up form and attaches the submit event listener.
  • Attempts to sign up the user using the name, email and password provided.

Now, let’s deploy the application to Vercel.

Deploy to Vercel

The code is now ready to deploy to Vercel. Use the following steps to deploy:

  • Start by creating a GitHub repository containing your app’s code.
  • Then, navigate to the Vercel Dashboard and create a New Project.
  • Link the new project to the GitHub repository you have just created.
  • In Settings, update the Environment Variables to match those in your local .env file.
  • Deploy! 🚀

References

Conclusion

In this guide, you enabled user authentication via credentials method with the help of Better Auth in an Astro application. You have also gained some experience with using middleware in Astro, and understanding how it can help you build dynamic user interfaces.

If you have any questions or comments, feel free to reach out to me on Twitter.

Learn More Astro vs Next.js: Choosing the Right Framework in 2024
Astro vs Next.js: Choosing the Right Framework in 2024 October 30, 2024
6 Essential Features Every Web Starter Kit Should Include
6 Essential Features Every Web Starter Kit Should Include October 26, 2024
Launch Fast with Astro 4.16
Launch Fast with Astro 4.16 October 23, 2024