## Introduction

[Better Auth](https://better-auth.com/) is a modern, open-source authentication solution for web apps. It's built with TypeScript and provides a simple and extensible auth experience with support for multiple database adapters, including Prisma.

In this guide, you'll wire Better Auth into a brand-new [Astro](https://astro.build/) app and persist users in a [Prisma Postgres](https://prisma.io/postgres) database. You can find a complete example of this guide on [GitHub](https://github.com/prisma/prisma-examples/tree/latest/orm/betterauth-astro).

## Prerequisites

- [Node.js 20+](https://nodejs.org)
- Basic familiarity with Astro and Prisma

## 1. Set up your project

Create a new Astro project:

```terminal
npm create astro@latest betterauth-astro-prisma
```
:::info

- *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:

```terminal
cd betterauth-astro-prisma
```

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

## 2. Set up Prisma

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

### 2.1. Install Prisma and dependencies

Install the necessary Prisma packages:

```terminal
npm install prisma tsx @types/pg --save-dev
npm install @prisma/client @prisma/adapter-pg dotenv pg
```

:::info

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](/orm/overview/databases/database-drivers).

:::

Once installed, initialize Prisma in your project:

```terminal
npx prisma init --db --output ../prisma/generated
```

:::info
You'll need to answer a few questions while setting up your Prisma Postgres database. Select the region closest to your location and a memorable name for your database like "My Better Auth Astro Project"
:::

This will create:

- A `prisma` directory with a `schema.prisma` file
- A Prisma Postgres database
- A `.env` file containing the `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`

### 2.2. Configure Prisma to load environment variables

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

```ts file=prisma.config.ts showLineNumbers
//add-start
import "dotenv/config";
//add-end
import { defineConfig, env } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
  },
  engine: "classic",
  datasource: {
    url: env("DATABASE_URL"),
  },
});
```

### 2.3. Generate the Prisma Client

Run the following command to generate the Prisma Client:

```terminal
npx prisma generate
```

### 2.4. Set up a global Prisma client

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.

```terminal
mkdir -p src/lib
touch src/lib/prisma.ts
```

Set up the Prisma client like this:

```ts file=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;
```

:::warning
We recommend using a connection pooler (like [Prisma Accelerate](https://www.prisma.io/accelerate)) to manage database connections efficiently.

If you choose not to use one, **avoid** instantiating `PrismaClient` globally in long-lived environments. Instead, create and dispose of the client per request to prevent exhausting your database connections.
:::

## 3. Set up Better Auth

Now it's time to integrate Better Auth for authentication.

### 3.1. Install and configure Better Auth

First, install the Better Auth core package:

```terminal
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.

```terminal
npx @better-auth/cli@latest secret
```

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

```dotenv file=.env showLineNumbers
# Better Auth
//add-start
BETTER_AUTH_SECRET=your-generated-secret
BETTER_AUTH_URL=http://localhost:4321
//add-end

# Prisma
DATABASE_URL="your-database-url"
```

:::info
Astro's default development server runs on port `4321`. If your application runs on a different port, update the `BETTER_AUTH_URL` accordingly.
:::

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

```terminal
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.

```ts file=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 [documentation](https://www.better-auth.com/docs/authentication/email-password).

### 3.2. Add Better Auth models to your schema

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:

```terminal
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")
}
```

### 3.3. Migrate the database

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

```terminal
npx prisma migrate dev --name add-auth-models
npx prisma generate
```

<br />

## 4. Set up the API routes

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:

```terminal
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.

```ts file=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:

```terminal
touch src/lib/auth-client.ts
```

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

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

export const authClient = createAuthClient();

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

## 5. Configure TypeScript definitions

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

```terminal
touch src/env.d.ts
```

Add the following type definitions:

```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;
  }
}

interface ImportMetaEnv {
  readonly DATABASE_URL: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}
```

## 6. Set up authentication middleware

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.

```terminal
touch src/middleware.ts
```

Add the following code:

```ts file=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();
});
```

## 7. Set up your pages

Now, let's 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`

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

### 7.1. Sign up page

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

```html file=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.

```html file=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>
      //add-start
      <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>
      //add-end
    </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.

```html file=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>
    //add-start
    <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>
    //add-end
  </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.

```html file=src/pages/sign-up/index.astro
---
export const prerender = false;

//add-start
if (Astro.locals.user?.id) return Astro.redirect("/dashboard");
//add-end
---

<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>
```

### 7.2. Sign in page

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

```html file=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.

```html file=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>
      //add-start
      <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>
      //add-end
    </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.

```html file=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>
    //add-start
    <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>
    //add-end
  </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.

```html file=src/pages/sign-in/index.astro
---
export const prerender = false;

//add-start
if (Astro.locals.user?.id) return Astro.redirect("/dashboard");
//add-end
---

<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>
```

### 7.3. Dashboard page

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

```html file=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.

```html file=src/pages/dashboard/index.astro
---
export const prerender = false;

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

<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.

```html file=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>
      //add-next-line
      <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.

```html file=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>
      //add-next-line
      <button id="signOutButton">Sign Out</button>
    </main>
    //add-start
    <script>
      import { authClient } from "../../lib/auth-client";
      document
        .getElementById("signOutButton")
        ?.addEventListener("click", async () => {
          await authClient.signOut();
          window.location.href = "/sign-in";
        });
    </script>
    //add-end
  </body>
</html>
```

### 7.4. Home page

Finally, update the home page to provide simple navigation. Replace the contents of `src/pages/index.astro` with the following:

```html file=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>
```

## 8. Test it out

Your application is now fully configured.

1. Start the development server to test it:

```terminal
npm run dev
```

2. Navigate to `http://localhost:4321` in your browser. You should see the home page with "Sign Up" and "Sign In" links.

3. 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.

4. To view the user data directly in your database, you can use Prisma Studio.

```terminal
npx prisma studio
```

5. This will open a new tab in your browser where you can see the `User`, `Session`, and `Account` tables and their contents.

:::success

Congratulations! You now have a fully functional authentication system built with Better Auth, Prisma, and Astro.

:::

## Next steps

- 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

## Further reading

- [Better Auth documentation](https://www.better-auth.com/docs)
- [Prisma documentation](/orm/overview/introduction)
- [Astro documentation](https://astro.build/docs)

---

# Integrate Prisma Postgres with Deno Deploy