|
| 1 | +--- |
| 2 | +sidebar_position: 1 |
| 3 | +--- |
| 4 | + |
| 5 | +import PackageInstall from '../../_components/PackageInstall'; |
| 6 | +import PackageExec from '../../_components/PackageExec'; |
| 7 | + |
| 8 | +# Better-Auth Integration |
| 9 | + |
| 10 | +[Better-Auth](https://better-auth.com) is a comprehensive authentication library for TypeScript applications. It supports a wide range of authentication providers and offers a flexible plugin system. |
| 11 | + |
| 12 | +This guide will show you how to integrate better-auth with ZenStack. |
| 13 | + |
| 14 | +## Using ZenStack as better-auth's database provider |
| 15 | + |
| 16 | +ZenStack provides a better-auth database adapter in the `@zenstackhq/better-auth` package. You can use it to configure better-auth to use ZenStack ORM as its database backend. |
| 17 | + |
| 18 | +### Installation |
| 19 | + |
| 20 | +<PackageInstall dependencies={["@zenstackhq/better-auth@next"]} /> |
| 21 | + |
| 22 | +### Better-Auth configuration |
| 23 | + |
| 24 | +Add the adapter to your better-auth configuration: |
| 25 | + |
| 26 | +```ts |
| 27 | +import { zenstackAdapter } from '@zenstackhq/better-auth'; |
| 28 | +import { db } from './db'; // your ZenStack ORM client |
| 29 | + |
| 30 | +const auth = new BetterAuth({ |
| 31 | + database: zenstackAdapter(db, { |
| 32 | + provider: 'postgresql', |
| 33 | + }), |
| 34 | + // other better-auth options... |
| 35 | +}); |
| 36 | +``` |
| 37 | + |
| 38 | +### Schema generation |
| 39 | + |
| 40 | +With the adapter set up, you can use better-auth CLI to populate its database models into your ZModel schema. Make sure you've installed the CLI: |
| 41 | + |
| 42 | +<PackageInstall devDependencies={["@better-auth/cli"]} /> |
| 43 | + |
| 44 | +Then, run the "generate" command to generate the schema: |
| 45 | + |
| 46 | +<PackageExec command="@better-auth/cli generate" /> |
| 47 | + |
| 48 | +Alternatively, you can refer to [better-auth schema documentation](https://www.better-auth.com/docs/concepts/database#core-schema) to manually add the necessary models. |
| 49 | + |
| 50 | +After the schema is configured, you can then use the regular ZenStack database schema migration workflow to push the schema to your database. |
| 51 | + |
| 52 | +## Integrating better-auth with ZenStack's access control |
| 53 | + |
| 54 | +### Creating user-bound ORM client |
| 55 | + |
| 56 | +ZenStack provides a powerful [access control system](../../orm/access-control/) that allows you to define fine-grained access policies for your data models. Enforcing access control often requires fetching the validated current user's identity, which is authentication system's responsibility. |
| 57 | + |
| 58 | +With better-auth, you can use the `auth.api.getSession()` API to get the current session on the server side. The following code shows an example with Next.js: |
| 59 | + |
| 60 | +```tsx |
| 61 | +import { betterAuth } from "better-auth"; |
| 62 | +import { headers } from "next/headers"; |
| 63 | + |
| 64 | +export const auth = betterAuth({ |
| 65 | + //... |
| 66 | +}) |
| 67 | + |
| 68 | +// calling get session on the server |
| 69 | +const {session} = await auth.api.getSession({ |
| 70 | + headers: await headers() // some endpoint might require headers |
| 71 | +}); |
| 72 | + |
| 73 | +// get the userId from session data |
| 74 | +const userId = session.userId; |
| 75 | +``` |
| 76 | + |
| 77 | +Then you can pass it to `ZenStackClient`'s `$setAuth()` method to get a user-bound ORM client. |
| 78 | + |
| 79 | +```tsx |
| 80 | +const userDb = db.$setAuth({ userId }); |
| 81 | +``` |
| 82 | + |
| 83 | +### Organization plugin support |
| 84 | + |
| 85 | +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. |
| 86 | + |
| 87 | +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: |
| 88 | + |
| 89 | +```tsx |
| 90 | + |
| 91 | + let organizationRole: string | undefined = undefined; |
| 92 | + const organizationId = session.activeOrganizationId; |
| 93 | + const org = await auth.api.getFullOrganization({ headers: reqHeaders }); |
| 94 | + if (org?.members) { |
| 95 | + const myMember = org.members.find( |
| 96 | + (m) => m.userId === session.userId |
| 97 | + ); |
| 98 | + organizationRole = myMember?.role; |
| 99 | + } |
| 100 | + |
| 101 | + // user identity with organization info |
| 102 | + const userContext = { |
| 103 | + userId: session.userId, |
| 104 | + organizationId, |
| 105 | + organizationRole, |
| 106 | + }; |
| 107 | +``` |
| 108 | + |
| 109 | +Then you can use the full `userContext` object to get a user-bound client. |
| 110 | + |
| 111 | +```tsx |
| 112 | +const userDb = db.$setAuth(userContext); |
| 113 | +``` |
| 114 | + |
| 115 | +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()`: |
| 116 | + |
| 117 | +```tsx |
| 118 | +type Auth { |
| 119 | + userId String @id |
| 120 | + organizationId String? |
| 121 | + organizationRole String? |
| 122 | + |
| 123 | + @@auth |
| 124 | +} |
| 125 | +``` |
| 126 | + |
| 127 | +Now you can access the organization information in policy rules: |
| 128 | + |
| 129 | +```tsx |
| 130 | +model Todo { |
| 131 | + ... |
| 132 | + organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade) |
| 133 | + organizationId String? @default(auth().organizationId) |
| 134 | + |
| 135 | + // deny access that don't belong to the user's active organization |
| 136 | + @@deny('all', auth().organizationId != organizationId) |
| 137 | + |
| 138 | + // full access to: list owner, org owner, and org admins |
| 139 | + @@allow('all', |
| 140 | + auth().userId == ownerId || |
| 141 | + auth().organizationRole == 'owner' || |
| 142 | + auth().organizationRole == 'admin') |
| 143 | +} |
| 144 | +``` |
| 145 | +
|
| 146 | +## Sample project |
| 147 | +
|
| 148 | +Here is a fully working multi-tenant sample project using better-auth, ZenStack v3, and Next.js: |
| 149 | +
|
| 150 | +[https://github.com/ymc9/better-auth-zenstack-multitenancy/tree/zenstack-v3](https://github.com/ymc9/better-auth-zenstack-multitenancy/tree/zenstack-v3) |
0 commit comments