Skip to main content
Prisma Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

Better Auth (with Astro)

Better Auth is an open-source authentication library for web apps. It's written in TypeScript, can be extended with plugins, and supports multiple database adapters, including Prisma.

In this guide, you'll wire Better Auth into a brand-new Astro app and persist users in a Prisma Postgres database. You can find a complete example of this guide on GitHub.

Create a new Astro project:

bunx create-astro betterauth-astro-prisma
Bash
pnpm create astro betterauth-astro-prisma
Bash
yarn create astro betterauth-astro-prisma
Bash
npm create astro@latest betterauth-astro-prisma

[!NOTE]

  • How would you like to start your new project? Use minimal (empty) template
  • Install dependencies? (recommended) Yes
  • Initialize a new git repository? (optional) Yes

Navigate to the project directory:

Bash
cd betterauth-astro-prisma

These selections will create a minimal Astro project with TypeScript for type safety.

Navigate to the project directory:

cd betterauth-astro-prisma

These selections will create a minimal Astro project with TypeScript for type safety.

Next, you'll add Prisma to your project to manage your database.

Install the necessary Prisma packages:

title="bun"
bun add prisma@prev tsx @types/pg --dev
pnpm
pnpm add prisma@prev tsx @types/pg --save-dev
yarn
yarn add prisma@prev tsx @types/pg --dev
npm
npm install prisma@prev tsx @types/pg --save-dev
bun add @prisma/client@7 @prisma/adapter-pg dotenv pg
Bash
pnpm add @prisma/client@7 @prisma/adapter-pg dotenv pg
Bash
yarn add @prisma/client@7 @prisma/adapter-pg dotenv pg
Bash
npm install @prisma/client@7 @prisma/adapter-pg dotenv pg

[!NOTE] If you are using a different database provider (MySQL, SQL Server, SQLite), install the corresponding driver adapter package instead of @prisma/adapter-pg. For more information, see Database drivers.

Once installed, initialize Prisma in your project:

Once installed, initialize Prisma in your project:

bunx --bun prisma init --output ../prisma/generated
Bash
pnpm prisma init --output ../prisma/generated
Bash
yarn prisma init --output ../prisma/generated
Bash
npx prisma init --output ../prisma/generated

[!NOTE] prisma init creates the Prisma scaffolding and a local DATABASE_URL. In the next step, you will create a Prisma Postgres database and replace that value with a direct postgres://... connection string.

This will create:

  • A prisma directory with a schema.prisma file
  • A .env file containing a local DATABASE_URL at the project root
  • A prisma.config.ts file for configuring Prisma
  • An output directory for the generated Prisma Client as prisma/generated

Create a Prisma Postgres database and replace the generated DATABASE_URL in your .env file with the postgres://... connection string from the CLI output:

This will create:

  • A prisma directory with a schema.prisma file
  • A .env file containing a local DATABASE_URL at the project root
  • A prisma.config.ts file for configuring Prisma
  • An output directory for the generated Prisma Client as prisma/generated

Create a Prisma Postgres database and replace the generated DATABASE_URL in your .env file with the postgres://... connection string from the CLI output:

title="bun"
bunx create-db
pnpm
pnpm dlx create-db
yarn
yarn dlx create-db
npm
npx create-db

To get access to the variables in the .env file, update your prisma.config.ts to import dotenv:

title="prisma.config.ts"
import "dotenv/config"; 

import { defineConfig, env } from "prisma/config";

export default defineConfig({

  schema: "prisma/schema.prisma",

  migrations: {

    path: "prisma/migrations",

  },

  engine: "classic",

  datasource: {

    url: env("DATABASE_URL"),

  },

});

Run the following command to generate the Prisma Client:

title="bun"
bunx prisma generate
pnpm
pnpm prisma generate
yarn
yarn prisma generate
npm
npx prisma generate

In the src directory, create a lib folder and a prisma.ts file inside it. This file will be used to create and export your Prisma Client instance.

mkdir -p src/lib

touch src/lib/prisma.ts

Set up the Prisma client like this:

title="src/lib/prisma.ts"
import { PrismaClient } from "../../prisma/generated/client";

import { PrismaPg } from "@prisma/adapter-pg";

const adapter = new PrismaPg({

  connectionString: import.meta.env.DATABASE_URL,

});

const prisma = new PrismaClient({

  adapter,

});

export default prisma;

Next, add Better Auth to handle sign-up, sign-in, and sessions.

First, install the Better Auth core package:

bun add better-auth
Bash
pnpm add better-auth
Bash
yarn add better-auth
Bash
npm install better-auth

Next, generate a secure secret that Better Auth will use to sign authentication tokens. This ensures your tokens cannot be tampered with.

Next, generate a secure secret that Better Auth will use to sign authentication tokens. This ensures your tokens cannot be tampered with.

title="bun"
bunx @better-auth/cli@latest secret
pnpm
pnpm dlx @better-auth/cli@latest secret
yarn
yarn dlx @better-auth/cli@latest secret
npm
npx @better-auth/cli@latest secret

Copy the generated secret and add it, along with your application's URL, to your .env file:

title=".env"
# Better Auth

BETTER_AUTH_SECRET=your-generated-secret

BETTER_AUTH_URL=http://localhost:4321

# Prisma

DATABASE_URL="your-database-url"

Now, create a configuration file for Better Auth. In the src/lib directory, create an auth.ts file:

touch src/lib/auth.ts

In this file, you'll configure Better Auth to use the Prisma adapter, which allows it to persist user and session data in your database. You will also enable email and password authentication.

title="src/lib/auth.ts"
import { betterAuth } from "better-auth";

import { prismaAdapter } from "better-auth/adapters/prisma";

import prisma from "./prisma";

export const auth = betterAuth({

  database: prismaAdapter(prisma, {

    provider: "postgresql",

  }),

  emailAndPassword: {

    enabled: true,

  },

});

Better Auth also supports other sign-in methods like social logins (Google, GitHub, etc.), which you can explore in their email and password documentation.

Better Auth provides a CLI command to automatically add the necessary authentication models (User, Session, Account, and Verification) to your schema.prisma file.

Run the following command:

bunx @better-auth/cli generate
Bash
pnpm dlx @better-auth/cli generate
Bash
yarn dlx @better-auth/cli generate
Bash
npx @better-auth/cli generate

[!NOTE] It will ask for confirmation to overwrite your existing Prisma schema. Select y.

This will add the following models:

prisma
model User {
  id            String    @id
  name          String
  email         String
  emailVerified Boolean
  image         String?
  createdAt     DateTime
  updatedAt     DateTime
  sessions      Session[]
  accounts      Account[]

  @@unique([email])
  @@map("user")
}

model Session {
  id        String   @id
  expiresAt DateTime
  token     String
  createdAt DateTime
  updatedAt DateTime
  ipAddress String?
  userAgent String?
  userId    String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([token])
  @@map("session")
}

model Account {
  id                    String    @id
  accountId             String
  providerId            String
  userId                String
  user                  User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  accessToken           String?
  refreshToken          String?
  idToken               String?
  accessTokenExpiresAt  DateTime?
  refreshTokenExpiresAt DateTime?
  scope                 String?
  password              String?
  createdAt             DateTime
  updatedAt             DateTime

  @@map("account")
}

model Verification {
  id         String    @id
  identifier String
  value      String
  expiresAt  DateTime
  createdAt  DateTime?
  updatedAt  DateTime?

  @@map("verification")
}

This will add the following models:

model User {

  id            String    @id

  name          String

  email         String

  emailVerified Boolean

  image         String?

  createdAt     DateTime

  updatedAt     DateTime

  sessions      Session[]

  accounts      Account[]

  @@unique([email])

  @@map("user")

}

model Session {

  id        String   @id

  expiresAt DateTime

  token     String

  createdAt DateTime

  updatedAt DateTime

  ipAddress String?

  userAgent String?

  userId    String

  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([token])

  @@map("session")

}

model Account {

  id                    String    @id

  accountId             String

  providerId            String

  userId                String

  user                  User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  accessToken           String?

  refreshToken          String?

  idToken               String?

  accessTokenExpiresAt  DateTime?

  refreshTokenExpiresAt DateTime?

  scope                 String?

  password              String?

  createdAt             DateTime

  updatedAt             DateTime

  @@map("account")

}

model Verification {

  id         String    @id

  identifier String

  value      String

  expiresAt  DateTime

  createdAt  DateTime?

  updatedAt  DateTime?

  @@map("verification")

}

With the new models in your schema, you need to update your database. Run a migration to create the corresponding tables:

title="bun"
bunx prisma migrate dev --name add-auth-models
pnpm
pnpm prisma migrate dev --name add-auth-models
yarn
yarn prisma migrate dev --name add-auth-models
npm
npx prisma migrate dev --name add-auth-models
bunx prisma generate
Bash
pnpm prisma generate
Bash
yarn prisma generate
Bash
npx prisma generate

Better Auth needs an API endpoint to handle authentication requests like sign-in, sign-up, and sign-out. You'll create a catch-all API route in Astro to handle all requests sent to /api/auth/[...all].

In the src/pages directory, create an api/auth folder structure and a [...all].ts file inside it:

mkdir -p src/pages/api/auth

touch 'src/pages/api/auth/[...all].ts'

Add the following code to the newly created [...all].ts file. This code uses the Better Auth handler to process authentication requests.

title="src/pages/api/auth/[...all].ts"
import { auth } from "../../../lib/auth";

import type { APIRoute } from "astro";

export const prerender = false; // Not needed in 'server' mode

export const ALL: APIRoute = async (ctx) => {

  return auth.handler(ctx.request);

};

Next, you'll need a client-side utility to interact with these endpoints from your Astro pages. In the src/lib directory, create an auth-client.ts file:

touch src/lib/auth-client.ts

Add the following code, which creates the client functions you'll use in your UI:

title="src/lib/auth-client.ts"
import { createAuthClient } from "better-auth/client";

export const authClient = createAuthClient();

export const { signIn, signUp, signOut, useSession } = authClient;

In the src directory, create an env.d.ts file to provide TypeScript definitions for environment variables and Astro locals:

touch src/env.d.ts

Add the following type definitions:

title="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;

  }

}

interface ImportMetaEnv {

  readonly DATABASE_URL: string;

}

interface ImportMeta {

  readonly env: ImportMetaEnv;

}

In the src directory, create a middleware.ts file to check authentication status on every request. This will make the user and session data available to all your pages.

touch src/middleware.ts

Add the following code:

title="src/middleware.ts"
import { auth } from "./lib/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();

});

Next, build the user interface for authentication. In the src/pages directory, create the following folder structure:

  • sign-up/index.astro
  • sign-in/index.astro
  • dashboard/index.astro
mkdir -p src/pages/{sign-up,sign-in,dashboard}

touch src/pages/{sign-up,sign-in,dashboard}/index.astro

This page allows new users to create an account. Start with the basic HTML structure in src/pages/sign-up/index.astro.

title="src/pages/sign-up/index.astro"
---

export const prerender = false;

---

<html lang="en">

  <head>

    <meta charset="utf-8" />

    <meta name="viewport" content="width=device-width" />

    <title>Sign Up</title>

  </head>

  <body>

    <main>

      <h1>Sign Up</h1>

    </main>

  </body>

</html>

Add a form with input fields for name, email, and password. This form will collect the user's registration information.

title="src/pages/sign-up/index.astro"
--export const prerender = false;--<html lang="en">  <head>    <meta charset="utf-8" />    <meta name="viewport" content="width=device-width" />    <title>Sign Up</title>  </head>  <body>    <main>      <h1>Sign Up</h1>      <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="/sign-in">Sign in here</a>.</p>    </main>  </body></html>

Now add a script to handle form submission. Import the authClient and add an event listener to the form that prevents the default submission behavior, extracts the form data, and calls the Better Auth sign-up method.

title="src/pages/sign-up/index.astro"
---

export const prerender = false;

---

<html lang="en">

  <head>

    <meta charset="utf-8" />

    <meta name="viewport" content="width=device-width" />

    <title>Sign Up</title>

  </head>

  <body>

    <main>

      <h1>Sign Up</h1>

      <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="/sign-in">Sign in here</a>.</p>

    </main>

    <script>

           import { authClient } from "../../lib/auth-client"; 

           document 

             .getElementById("signup-form") 

             ?.addEventListener("submit", async (event) => { 

               event.preventDefault(); 

               const formData = new FormData(event.target as HTMLFormElement); 

               const name = formData.get("name") as string; 

               const email = formData.get("email") as string; 

               const password = formData.get("password") as string; 

               const tmp = await authClient.signUp.email({ 

                 name, 

                 email, 

                 password, 

               }); 

               console.log(tmp); 

               if (Boolean(tmp.error) === false) window.location.href = "/dashboard"; 

             }); 

    </script>

  </body>

</html>

Finally, add a server-side check to redirect authenticated users away from this page. If a user is already signed in, they should be redirected to the dashboard instead.

title="src/pages/sign-up/index.astro"
--export const prerender = false;if (Astro.locals.user?.id) return Astro.redirect("/dashboard");--<html lang="en">  <head>    <meta charset="utf-8" />    <meta name="viewport" content="width=device-width" />    <title>Sign Up</title>  </head>  <body>    <main>      <h1>Sign Up</h1>      <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="/sign-in">Sign in here</a>.</p>    </main>    <script>      import { authClient } from "../../lib/auth-client";      document        .getElementById("signup-form")        ?.addEventListener("submit", async (event) => {          event.preventDefault();          const formData = new FormData(event.target as HTMLFormElement);          const name = formData.get("name") as string;          const email = formData.get("email") as string;          const password = formData.get("password") as string;          const tmp = await authClient.signUp.email({            name,            email,            password,          });          console.log(tmp);          if (Boolean(tmp.error) === false) window.location.href = "/dashboard";        });    </script>  </body></html>

This page allows existing users to authenticate. Start with the basic HTML structure in src/pages/sign-in/index.astro.

title="src/pages/sign-in/index.astro"
---

export const prerender = false;

---

<html lang="en">

  <head>

    <meta charset="utf-8" />

    <meta name="viewport" content="width=device-width" />

    <title>Sign In</title>

  </head>

  <body>

    <main>

      <h1>Sign In</h1>

    </main>

  </body>

</html>

Add a form with input fields for email and password. This form will collect the user's credentials.

title="src/pages/sign-in/index.astro"
--export const prerender = false;--<html lang="en">  <head>    <meta charset="utf-8" />    <meta name="viewport" content="width=device-width" />    <title>Sign In</title>  </head>  <body>    <main>      <h1>Sign In</h1>      <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="/sign-up">Sign up here</a>.</p>    </main>  </body></html>

Now add a script to handle form submission. Import the authClient and add an event listener that prevents default submission, extracts the form data, and calls the Better Auth sign-in method.

title="src/pages/sign-in/index.astro"
---

export const prerender = false;

---

<html lang="en">

  <head>

    <meta charset="utf-8" />

    <meta name="viewport" content="width=device-width" />

    <title>Sign In</title>

  </head>

  <body>

    <main>

      <h1>Sign In</h1>

      <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="/sign-up">Sign up here</a>.</p>

    </main>

    <script>

           import { authClient } from "../../lib/auth-client"; 

           document 

             .getElementById("signin-form") 

             ?.addEventListener("submit", async (event) => { 

               event.preventDefault(); 

               const formData = new FormData(event.target as HTMLFormElement); 

               const email = formData.get("email") as string; 

               const password = formData.get("password") as string; 

               const tmp = await authClient.signIn.email({ 

                 email, 

                 password, 

               }); 

               if (Boolean(tmp.error) === false) window.location.href = "/dashboard"; 

             }); 

    </script>

  </body>

</html>

Finally, add a server-side check to redirect authenticated users away from this page. If a user is already signed in, they should be redirected to the dashboard instead.

title="src/pages/sign-in/index.astro"
--export const prerender = false;if (Astro.locals.user?.id) return Astro.redirect("/dashboard");--<html lang="en">  <head>    <meta charset="utf-8" />    <meta name="viewport" content="width=device-width" />    <title>Sign In</title>  </head>  <body>    <main>      <h1>Sign In</h1>      <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="/sign-up">Sign up here</a>.</p>    </main>    <script>      import { authClient } from "../../lib/auth-client";      document        .getElementById("signin-form")        ?.addEventListener("submit", async (event) => {          event.preventDefault();          const formData = new FormData(event.target as HTMLFormElement);          const email = formData.get("email") as string;          const password = formData.get("password") as string;          const tmp = await authClient.signIn.email({            email,            password,          });          if (Boolean(tmp.error) === false) window.location.href = "/dashboard";        });    </script>  </body></html>

This is the protected page for authenticated users. Start with the basic HTML structure in src/pages/dashboard/index.astro.

title="src/pages/dashboard/index.astro"
---

export const prerender = false;

---

<html lang="en">

  <head>

    <meta charset="utf-8" />

    <meta name="viewport" content="width=device-width" />

    <title>Dashboard</title>

  </head>

  <body>

    <main>

      <h1>Dashboard</h1>

    </main>

  </body>

</html>

Add a server-side check to protect this route. If the user is not authenticated, redirect them to the sign-in page.

title="src/pages/dashboard/index.astro"
--export const prerender = false;if (!Astro.locals.user?.id) return Astro.redirect("/sign-in");--<html lang="en">  <head>    <meta charset="utf-8" />    <meta name="viewport" content="width=device-width" />    <title>Dashboard</title>  </head>  <body>    <main>      <h1>Dashboard</h1>    </main>  </body></html>

Now display the authenticated user's information. The Astro.locals.user object contains the user data that was set by the middleware.

title="src/pages/dashboard/index.astro"
---

export const prerender = false;

if (!Astro.locals.user?.id) return Astro.redirect("/sign-in");

---

<html lang="en">

  <head>

    <meta charset="utf-8" />

    <meta name="viewport" content="width=device-width" />

    <title>Dashboard</title>

  </head>

  <body>

    <main>

      <h1>Dashboard</h1>

      <pre>{JSON.stringify(Astro.locals.user, null, 2)}</pre>

    </main>

  </body>

</html>

Finally, add a sign-out button. Import the authClient and add a button that calls the sign-out method, allowing the user to log out and be redirected to the sign-in page.

title="src/pages/dashboard/index.astro"
--export const prerender = false;if (!Astro.locals.user?.id) return Astro.redirect("/sign-in");--<html lang="en">  <head>    <meta charset="utf-8" />    <meta name="viewport" content="width=device-width" />    <title>Dashboard</title>  </head>  <body>    <main>      <h1>Dashboard</h1>      <pre>{JSON.stringify(Astro.locals.user, null, 2)}</pre>      <button id="signOutButton">Sign Out</button>    </main>    <script>      import { authClient } from "../../lib/auth-client";       document         .getElementById("signOutButton")         ?.addEventListener("click", async () => {          await authClient.signOut();           window.location.href = "/sign-in";         });     </script>  </body></html>

Finally, update the home page to link to the sign-up, sign-in, and dashboard pages. Replace the contents of src/pages/index.astro with the following:

title="src/pages/index.astro"
---

export const prerender = false;

---

<html lang="en">

  <head>

    <meta charset="utf-8" />

    <meta name="viewport" content="width=device-width" />

    <title>Better Auth + Astro + Prisma</title>

  </head>

  <body>

    <main>

      <h1>Better Auth + Astro + Prisma</h1>

      { Astro.locals.user ? (

      <div>

        <p>Welcome back, {Astro.locals.user.name}!</p>

        <a href="/dashboard">Go to Dashboard</a>

      </div>

      ) : (

      <div>

        <a href="/sign-up">Sign Up</a>

        <a href="/sign-in">Sign In</a>

      </div>

      ) }

    </main>

  </body>

</html>

Your application is now fully configured.

  1. Start the development server to test it:
bun run dev
Bash
pnpm run dev
Bash
yarn dev
Bash
npm run dev
  1. Navigate to http://localhost:4321 in your browser. You should see the home page with "Sign Up" and "Sign In" links.
  2. Click on Sign Up, create a new account, and you should be redirected to the dashboard. You can then sign out and sign back in.
  3. To view the user data directly in your database, you can use Prisma Studio.
  1. Navigate to http://localhost:4321 in your browser. You should see the home page with "Sign Up" and "Sign In" links.
  2. Click on Sign Up, create a new account, and you should be redirected to the dashboard. You can then sign out and sign back in.
  3. To view the user data directly in your database, you can use Prisma Studio.
bunx prisma studio
Bash
pnpm prisma studio
Bash
yarn prisma studio
Bash
npx prisma studio
  1. This will open a new tab in your browser where you can see the User, Session, and Account tables and their contents.

[!TIP] You now have a working authentication system built with Better Auth, Prisma, and Astro.

  1. This will open a new tab in your browser where you can see the User, Session, and Account tables and their contents.
  • Add support for social login or magic links
  • Implement password reset and email verification
  • Add user profile and account management pages
  • Deploy to Vercel or Netlify and secure your environment variables
  • Extend your Prisma schema with custom application models
Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu