Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/quick-start/authentication/auth0.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: Integrating with Auth0.
sidebar_position: 6
sidebar_position: 5
sidebar_label: Auth0
---

Expand Down
129 changes: 129 additions & 0 deletions docs/quick-start/authentication/better-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
---
description: Integrating with Better Auth
sidebar_position: 2
sidebar_label: Better Auth
---

# Integrating With Better Auth

[Better Auth](https://better-auth.com/) is an emerging open-source TypeScript authentication framework that offers a comprehensive set of features and great extensibility.

## Preparation

Follow Better Auth's installation [guide](https://www.better-auth.com/docs/installation#configure-database) using the Prisma adapter:

```tsx
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "sqlite", // or "mysql", "postgresql", ...etc
}),
});
```

Running Better Auth CLI to generate the Auth-related model in the Prisma schema:

```bash
npx @better-auth/cli generate
```

## Integrate with ZenStack

Follow the [installation guide](https://zenstack.dev/docs/install) to install ZenStack in your project.

Integration with ZenStack is all about obtaining the user's identity and utilizing it to create an enhanced `PrismaClient`. On the server side, Better Auth exposes that through the `api` object of the `auth` instance. For example, here is how to get that in Next.js:

```tsx
import { betterAuth } from "better-auth";
import { headers } from "next/headers";

export const auth = betterAuth({
//...
})

// calling get session on the server
const {session} = await auth.api.getSession({
headers: await headers() // some endpoint might require headers
});

// get the userId from session data
const userId = session.userId;
```

Then you can pass it to ZenStack's [`enhance()`](https://zenstack.dev/docs/reference/runtime-api#enhance) API to create an enhanced `PrismaClient` that automatically enforces access policies.

```tsx
const db = enhance(prisma, { user: {id: userId} });
```

## Organization Plugin Support

Better Auth has a powerful plugin system that allows you to add new features that contribute extensions across the entire stack - data model, backend API, and frontend hooks. A good example is the [Organization plugin](https://www.better-auth.com/docs/plugins/organization), which sets the foundation for implementing multi-tenant apps with access control.

After enabling the Organization plugin and running the CLI to generate the additional models and fields in the schema, you can use the code below on the server side to get the organization info together with the user identity:

```tsx

let organizationRole: string | undefined = undefined;
const organizationId = session.activeOrganizationId;
const org = await auth.api.getFullOrganization({ headers: reqHeaders });
if (org?.members) {
const myMember = org.members.find(
(m) => m.userId === session.userId
);
organizationRole = myMember?.role;
}
Comment on lines +70 to +79
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

reqHeaders is undefined – align this snippet with the previous one

reqHeaders hasn’t been introduced anywhere, which will confuse readers. Either reuse headers() from the earlier example or show how reqHeaders is derived.

-const org = await auth.api.getFullOrganization({ headers: reqHeaders });
+const org = await auth.api.getFullOrganization({
+    headers: headers(), // reuse Next.js request headers
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let organizationRole: string | undefined = undefined;
const organizationId = session.activeOrganizationId;
const org = await auth.api.getFullOrganization({ headers: reqHeaders });
if (org?.members) {
const myMember = org.members.find(
(m) => m.userId === session.userId
);
organizationRole = myMember?.role;
}
let organizationRole: string | undefined = undefined;
const organizationId = session.activeOrganizationId;
const org = await auth.api.getFullOrganization({
headers: headers(), // reuse Next.js request headers
});
if (org?.members) {
const myMember = org.members.find(
(m) => m.userId === session.userId
);
organizationRole = myMember?.role;
}
🤖 Prompt for AI Agents
In docs/quick-start/authentication/better-auth.md around lines 70 to 79, the
variable reqHeaders is used but never defined, causing confusion. Fix this by
either replacing reqHeaders with headers() as used in the previous snippet or
explicitly define reqHeaders before this code block to show how it is derived,
ensuring consistency and clarity for readers.


// user identity with organization info
const userContext = {
userId: session.userId,
organizationId,
organizationRole,
};
```

:::info
The Better Auth CLI will only update the `schema.prisma` file, if you add the Organization plugin after installing the ZenStack, you need to copy the change to `schema.zmodel` too.
:::

Then you can pass the full `userContext` object to the `enhanced` client:

```tsx
const db = enhance(prisma, { user: userContext });
```

The user context will be accessible in ZModel policy rules via the special `auth()` function. To get it to work, let's add a type in ZModel to define the shape of `auth()`:

```tsx
type Auth {
userId String @id
organizationId String?
organizationRole String?
@@auth
}
```

Here is how you could access it in the access policies:

```tsx
model ToDo {
...
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
organizationId String? @default(auth().organizationId) @allow('update', false)

// deny access that don't belong to the user's active organization
@@deny('all', auth().organizationId != organizationId)

// full access to: list owner, org owner, and org admins
@@allow('all',
auth().userId == ownerId ||
auth().organizationRole == 'owner' ||
auth().organizationRole == 'admin')
}
```

You can find a complete sample of a multi-tenant Todo app code [here](https://github.com/ymc9/better-auth-zenstack-multitenancy).
2 changes: 1 addition & 1 deletion docs/quick-start/authentication/clerk.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: Integrating with Clerk.
sidebar_position: 2
sidebar_position: 3
sidebar_label: Clerk
---

Expand Down
2 changes: 1 addition & 1 deletion docs/quick-start/authentication/lucia.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: Integrating with Lucia.
sidebar_position: 5
sidebar_position: 6
sidebar_label: Lucia
---

Expand Down
2 changes: 1 addition & 1 deletion docs/quick-start/authentication/supabase.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: Integrating with Supabase Auth.
sidebar_position: 3
sidebar_position: 4
sidebar_label: Supabase Auth
---

Expand Down