Multi-Tenant userbases / updatable user schema #575

Closed
opened 2026-03-13 07:53:54 -05:00 by GiteaMirror · 29 comments
Owner

Originally created by @robskinney on GitHub (Jan 19, 2025).

Is this suited for github?

  • Yes, this is suited for github

Hi, I'm looking to create a B2B2C SaaS application with a couple requirements:

  1. Platform users (unique by email) can create organizations, invite others to organizations, and create applications that are deployed on subdomains of my site.
  2. Application users (unique by app/tenant Id <-> email) can join and interact with the applications deployed on subdomains. These users should be able to have their own passwords across applications on the platform.

I was able to update the user schema and enjoy type-safety with the inferAdditionalFields() plugin, but now I'm facing the issue of actually allowing users to sign up under these different tenants. Currently, this error is arising: [Better Auth]: Sign-up attempt for existing email: <email>@<domain>.com.

I did some looking, and it looks like this is verified within the /api/routes/sign-up.ts file, which has the following bit of code (using the findUserByEmail function):

const dbUser = await ctx.context.internalAdapter.findUserByEmail(email);
if (dbUser?.user) {
    ctx.context.logger.info(`Sign-up attempt for existing email: ${email}`);
        throw new APIError("UNPROCESSABLE_ENTITY", {
            message: BASE_ERROR_CODES.USER_ALREADY_EXISTS,
	});
}

Describe the solution you'd like

I'd like the ability to generalize this, allowing the user to identify additional unique constraint fields. I think this could be done with some small-ish (hopefully) updates:

  1. Update the additionalFields functionality on the User table to allow for a "unique" boolean. The email address would remain a unique constraint, but any additional fields marked in the "unique" boolean are also validated. I don't think the inferAdditionalFields() plugin would need updated, as there is already an option to set fields as "required."
export const auth = betterAuth({
    user: {
      additionalFields: {
        tenantId: {
          type: "string",
          required: true,
          unique: true, //marking unique as true indicates that it should be validated upon insert or update
          input: true,
        },
      },
    }
  });
  1. Repurpose the findUserByEmail() function in /db/internal-adapter.ts to allow for additional fields to search by (AI pseudo-code, definitely can be improved. I see there are updateWithHooks() and createWithHooks() functions -- maybe a getWithHooks() would allow us to do this more efficiently?).
findUserByEmail: async (
    email: string,
    options?: { includeAccounts: boolean; additionalFields?: { key: string; value: any }[] },
) => {
    // starting with baseline where clause with user email
    const whereClauses = [
        {
            value: email.toLowerCase(),
            field: "email",
        },
    ];

    // if additional fields are passed, add them to the where clause
    if (options?.additionalFields) {
        options.additionalFields.forEach(({ key, value }) => {
            whereClauses.push({
                value,
                field: key,
            });
        });
    }

    // check if user exists based on where clauses
    const user = await adapter.findOne<User>({
        model: "user",
        where: whereClauses,
    });

    if (!user) return null;
    if (options?.includeAccounts) {
        const accounts = await adapter.findMany<Account>({
            model: "account",
            where: [
                {
                    value: user.id,
                    field: "userId",
                },
            ],
        });
        return {
            user,
            accounts,
        };
    }
    return {
        user,
        accounts: [],
    };
},
  
  1. Update the sign up API route /api/routes/sign-up.ts to call the updated function with additionalFields provided, as long as they have a "unique" boolean set to true.
const dbUser = await ctx.context.internalAdapter.findUserByEmail(email, options: {additionalFields: ...additionalFields});
if (dbUser?.user) {
    ctx.context.logger.info(`Sign-up attempt for existing email: ${email}`); // update to indicate the unique constraint that failed rather than solely email
        throw new APIError("UNPROCESSABLE_ENTITY", {
            message: BASE_ERROR_CODES.USER_ALREADY_EXISTS,
	});
}

I'm sure there's more that I'm not thinking of, I didn't look into it too deep. Is this something that would be considered? I'd happily spend some time trying to put something together.

Describe alternatives you've considered

I'm open to suggestions. I've tried so many authentication libraries and services and none of them seem to have this ability. If you have any ideas to make something like this work, I'd greatly appreciate it! I've been having a hard time finding something to apply for this scenario in my Next.js app.

Additional context

No response

Originally created by @robskinney on GitHub (Jan 19, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### Is your feature request related to a problem? Please describe. Hi, I'm looking to create a B2B2C SaaS application with a couple requirements: 1. Platform users (unique by email) can create organizations, invite others to organizations, and create applications that are deployed on subdomains of my site. 2. Application users (unique by app/tenant Id <-> email) can join and interact with the applications deployed on subdomains. These users should be able to have their own passwords across applications on the platform. I was able to update the user schema and enjoy type-safety with the ```inferAdditionalFields()``` plugin, but now I'm facing the issue of actually _allowing_ users to sign up under these different tenants. Currently, this error is arising: ```[Better Auth]: Sign-up attempt for existing email: <email>@<domain>.com```. I did some looking, and it looks like this is verified within the ```/api/routes/sign-up.ts``` file, which has the following bit of code (using the ```findUserByEmail``` function): ```typescript const dbUser = await ctx.context.internalAdapter.findUserByEmail(email); if (dbUser?.user) { ctx.context.logger.info(`Sign-up attempt for existing email: ${email}`); throw new APIError("UNPROCESSABLE_ENTITY", { message: BASE_ERROR_CODES.USER_ALREADY_EXISTS, }); } ``` ### Describe the solution you'd like I'd like the ability to generalize this, allowing the user to identify _additional_ unique constraint fields. I think this could be done with some small-ish (hopefully) updates: 1. Update the ```additionalFields``` functionality on the User table to allow for a "unique" boolean. The email address would remain a unique constraint, but any additional fields marked in the "unique" boolean are also validated. I don't think the ```inferAdditionalFields()``` plugin would need updated, as there is already an option to set fields as "required." ```typescript export const auth = betterAuth({ user: { additionalFields: { tenantId: { type: "string", required: true, unique: true, //marking unique as true indicates that it should be validated upon insert or update input: true, }, }, } }); ``` 2. Repurpose the ```findUserByEmail()``` function in ```/db/internal-adapter.ts``` to allow for additional fields to search by (AI pseudo-code, definitely can be improved. I see there are ```updateWithHooks()``` and ```createWithHooks()``` functions -- maybe a ```getWithHooks()``` would allow us to do this more efficiently?). ```typescript findUserByEmail: async ( email: string, options?: { includeAccounts: boolean; additionalFields?: { key: string; value: any }[] }, ) => { // starting with baseline where clause with user email const whereClauses = [ { value: email.toLowerCase(), field: "email", }, ]; // if additional fields are passed, add them to the where clause if (options?.additionalFields) { options.additionalFields.forEach(({ key, value }) => { whereClauses.push({ value, field: key, }); }); } // check if user exists based on where clauses const user = await adapter.findOne<User>({ model: "user", where: whereClauses, }); if (!user) return null; if (options?.includeAccounts) { const accounts = await adapter.findMany<Account>({ model: "account", where: [ { value: user.id, field: "userId", }, ], }); return { user, accounts, }; } return { user, accounts: [], }; }, ``` 3. Update the sign up API route ```/api/routes/sign-up.ts``` to call the updated function with additionalFields provided, as long as they have a "unique" boolean set to true. ```typescript const dbUser = await ctx.context.internalAdapter.findUserByEmail(email, options: {additionalFields: ...additionalFields}); if (dbUser?.user) { ctx.context.logger.info(`Sign-up attempt for existing email: ${email}`); // update to indicate the unique constraint that failed rather than solely email throw new APIError("UNPROCESSABLE_ENTITY", { message: BASE_ERROR_CODES.USER_ALREADY_EXISTS, }); } ``` I'm sure there's more that I'm not thinking of, I didn't look into it too deep. Is this something that would be considered? I'd happily spend some time trying to put something together. ### Describe alternatives you've considered I'm open to suggestions. I've tried so many authentication libraries and services and none of them seem to have this ability. If you have any ideas to make something like this work, I'd greatly appreciate it! I've been having a hard time finding something to apply for this scenario in my Next.js app. ### Additional context _No response_
GiteaMirror added the enhancement label 2026-03-13 07:53:54 -05:00
Author
Owner

@terehov commented on GitHub (Jan 24, 2025):

Thank you, nicely written. We have the exact same use case and even thought of (or actually even started) building it on our own, before I ran into this library. better-auth ticks most of our boxes, but this main one is still missing, however seems to be doable with a little extra effort.
We think an email address should not be used as a sole identifier, but rather be something more like a "contact-email", while the actual identifier allow for a multi-column uniqueness with a tenant_id.

@terehov commented on GitHub (Jan 24, 2025): Thank you, nicely written. We have the exact same use case and even thought of (or actually even started) building it on our own, before I ran into this library. better-auth ticks most of our boxes, but this main one is still missing, however seems to be doable with a little extra effort. We think an email address should not be used as a sole identifier, but rather be something more like a "contact-email", while the actual identifier allow for a multi-column uniqueness with a `tenant_id`.
Author
Owner

@robskinney commented on GitHub (Jan 28, 2025):

Thank you, nicely written. We have the exact same use case and even thought of (or actually even started) building it on our own, before I ran into this library. better-auth ticks most of our boxes, but this main one is still missing, however seems to be doable with a little extra effort.
We think an email address should not be used as a sole identifier, but rather be something more like a "contact-email", while the actual identifier allow for a multi-column uniqueness with a tenant_id.

Thank you! Were you able to find a solution / workaround? I'm going to try to find some time in the next few weeks to implement something like this.

@robskinney commented on GitHub (Jan 28, 2025): > Thank you, nicely written. We have the exact same use case and even thought of (or actually even started) building it on our own, before I ran into this library. better-auth ticks most of our boxes, but this main one is still missing, however seems to be doable with a little extra effort. > We think an email address should not be used as a sole identifier, but rather be something more like a "contact-email", while the actual identifier allow for a multi-column uniqueness with a `tenant_id`. > > > Thank you! Were you able to find a solution / workaround? I'm going to try to find some time in the next few weeks to implement something like this.
Author
Owner

@okxiaoliang4 commented on GitHub (Jan 30, 2025):

I am building an 'application' schema for this case, Application can create many orgs, orgs can create multiple 'application'.

And create a 'drizzle-adapter.ts' cover the CRUD where condition.

I think your idea is greater.

Are you doing a PR for this?

@okxiaoliang4 commented on GitHub (Jan 30, 2025): I am building an 'application' schema for this case, Application can create many orgs, orgs can create multiple 'application'. And create a 'drizzle-adapter.ts' cover the CRUD where condition. I think your idea is greater. Are you doing a PR for this?
Author
Owner

@terehov commented on GitHub (Jan 30, 2025):

Thank you! Were you able to find a solution / workaround? I'm going to try to find some time in the next few weeks to implement something like this.

We decided to proceed with our own solution. Extending better-auth like that felt a bit hacky at the moment. First of all we use Deno, so the CLI tool didn’t work. Then we want to support multiple domains and custom configuration per tenant.

@terehov commented on GitHub (Jan 30, 2025): > Thank you! Were you able to find a solution / workaround? I'm going to try to find some time in the next few weeks to implement something like this. We decided to proceed with our own solution. Extending better-auth like that felt a bit hacky at the moment. First of all we use Deno, so the CLI tool didn’t work. Then we want to support multiple domains and custom configuration per tenant.
Author
Owner

@kracas commented on GitHub (Feb 4, 2025):

We also have a very similar use case.
Another, maybe a bit simpler solution would be to allow passing custom internalAdapter methods in the hooks. Changing and passing context is already allowed. This could look something like this:

import { betterAuth } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";
 
export const auth = betterAuth({
    hooks: {
        before: createAuthMiddleware(async (ctx) => {
            if (ctx.path === "/sign-in/email") {
                return {
                    context: {
                        ...ctx,
                        internalAdapter: {
                            findUserByEmail: async (email) => {
                                // Custom logic, where tenantId could come from the request

                                return {
                                    user,
                                    accounts
                                }
                            }
                        }
                    }
                };
            }
        }),
    },
});

And then this custom findUserByEmail would be used for this specific call instead of the one from createInternalAdapter.

@kracas commented on GitHub (Feb 4, 2025): We also have a very similar use case. Another, maybe a bit simpler solution would be to allow passing custom `internalAdapter` methods in the hooks. Changing and passing context is already allowed. This could look something like this: ```typescript import { betterAuth } from "better-auth"; import { createAuthMiddleware } from "better-auth/api"; export const auth = betterAuth({ hooks: { before: createAuthMiddleware(async (ctx) => { if (ctx.path === "/sign-in/email") { return { context: { ...ctx, internalAdapter: { findUserByEmail: async (email) => { // Custom logic, where tenantId could come from the request return { user, accounts } } } } }; } }), }, }); ``` And then this custom `findUserByEmail` would be used for this specific call instead of the one from `createInternalAdapter`.
Author
Owner

@arorajatin commented on GitHub (Mar 6, 2025):

Was anyone able to find a solution for this?

@arorajatin commented on GitHub (Mar 6, 2025): Was anyone able to find a solution for this?
Author
Owner

@Bekacru commented on GitHub (Mar 7, 2025):

Another thing you might be able to do to differentiate emails is to add a unique identifier to the email address itself. For example, if the email the user is trying to sign up with is user@email.com, you can use tenantId!user-@email.com instead. Then, you can use hooks and a util function to handle conversion.

@Bekacru commented on GitHub (Mar 7, 2025): Another thing you might be able to do to differentiate emails is to add a unique identifier to the email address itself. For example, if the email the user is trying to sign up with is `user@email.com`, you can use `tenantId!user-@email.com` instead. Then, you can use hooks and a util function to handle conversion.
Author
Owner

@jirizavadil commented on GitHub (Mar 14, 2025):

I'm in a similar position as @robskinney, only i have 3 cols (orgId, spaceId, environmentId) instead of one (tenantId) , so the proposed solution using additionalFields would be perfect for my case.

Prefixing the email as proposed by @Bekacru kinda works, but since the emails are validated by zod, instead of using just !, | or ~, i had to go for ---. Also it physically hurts )

@Bekacru can you confirm it would be possible to achieve the OP's proposal via a new tenant plugin or does it have to be done internally?

@jirizavadil commented on GitHub (Mar 14, 2025): I'm in a similar position as @robskinney, only i have 3 cols (`orgId`, `spaceId`, `environmentId`) instead of one (`tenantId`) , so the proposed solution using `additionalFields` would be perfect for my case. Prefixing the email as proposed by @Bekacru kinda works, but since the emails are validated by zod, instead of using just `!`, `|` or `~`, i had to go for `---`. Also it physically hurts ) @Bekacru can you confirm it would be possible to achieve the OP's proposal via a new `tenant` plugin or does it have to be done internally?
Author
Owner

@codylittle commented on GitHub (Mar 25, 2025):

We're in the process of migrating to Better Auth from AuthJS and are looking at either creating our own tenant plugin, or wrapping the existing Organization plugin since there is already support for SSO & teams.

However, a separate tenant plugin which orgs can exist within would be excellent.

@codylittle commented on GitHub (Mar 25, 2025): We're in the process of migrating to Better Auth from AuthJS and are looking at either creating our own `tenant` plugin, or wrapping the existing `Organization` plugin since there is already support for SSO & teams. However, a separate `tenant` plugin which orgs can exist within would be excellent.
Author
Owner

@mcorbelli commented on GitHub (Apr 5, 2025):

@codylittle Will the plugin by any chance be publicly available?

@mcorbelli commented on GitHub (Apr 5, 2025): @codylittle Will the plugin by any chance be publicly available?
Author
Owner

@codylittle commented on GitHub (Apr 7, 2025):

Our current implementation contains a lot of business-specific logic. But we will look at refactoring to be more generic and publish if similar functionality isn't implemented into Better-Auth

@codylittle commented on GitHub (Apr 7, 2025): Our current implementation contains a lot of business-specific logic. But we will look at refactoring to be more generic and publish if similar functionality isn't implemented into Better-Auth
Author
Owner

@mcorbelli commented on GitHub (Apr 7, 2025):

@codylittle thanks

@mcorbelli commented on GitHub (Apr 7, 2025): @codylittle thanks
Author
Owner

@joshuadutton commented on GitHub (May 16, 2025):

Just jumping in to add that I need this too. Let me know if any of you have made more progress on this. I'm also looking at writing a plugin that I could make publicly available. I've written my own auth library that already does this, so I'm also considering sticking with that for now, but it doesn't have all of the features of Better Auth, including some that I need now and don't have implemented yet in my own. It's seems worth it to me to leverage Better Auth with a plugin.

@joshuadutton commented on GitHub (May 16, 2025): Just jumping in to add that I need this too. Let me know if any of you have made more progress on this. I'm also looking at writing a plugin that I could make publicly available. I've written my own auth library that already does this, so I'm also considering sticking with that for now, but it doesn't have all of the features of Better Auth, including some that I need now and don't have implemented yet in my own. It's seems worth it to me to leverage Better Auth with a plugin.
Author
Owner

@joshuadutton commented on GitHub (May 16, 2025):

Adding some other related resources I found:

Discussion with @ping-maxwell about this issue: https://www.answeroverflow.com/m/1362421737203175544

PR for a WIP fix by @PodkopovP: https://github.com/better-auth/better-auth/pull/2342

I'm currently exploring the following:

  1. Trying the composite email solution posted by @Bekacru but also include tenantId and tenantEmail to my user schema.
  2. Testing the above PR to see if that works for me. If it does, I will add to it and make another PR to try to push that along.
  3. Look into creating a plugin to solve this. But it looks like the plugin would need the expanded functionality for findUserByEmail that the PR addresses anyway.
@joshuadutton commented on GitHub (May 16, 2025): Adding some other related resources I found: Discussion with @ping-maxwell about this issue: https://www.answeroverflow.com/m/1362421737203175544 PR for a WIP fix by @PodkopovP: https://github.com/better-auth/better-auth/pull/2342 I'm currently exploring the following: 1. Trying the composite email solution posted by @Bekacru but also include `tenantId` and `tenantEmail` to my user schema. 2. Testing the above PR to see if that works for me. If it does, I will add to it and make another PR to try to push that along. 3. Look into creating a plugin to solve this. But it looks like the plugin would need the expanded functionality for `findUserByEmail` that the PR addresses anyway.
Author
Owner

@PodkopovP commented on GitHub (May 17, 2025):

Adding some other related resources I found:

Discussion with @ping-maxwell about this issue: https://www.answeroverflow.com/m/1362421737203175544

PR for a WIP fix by @PodkopovP: https://github.com/better-auth/better-auth/pull/2342

I'm currently exploring the following:

  1. Trying the composite email solution posted by @Bekacru but also include tenantId and tenantEmail to my user schema.
  2. Testing the above PR to see if that works for me. If it does, I will add to it and make another PR to try to push that along.
  3. Look into creating a plugin to solve this. But it looks like the plugin would need the expanded functionality for findUserByEmail that the PR addresses anyway.

My PR was more of a proof of functionality I wanted to share rather than a full fledged solution - it can definitely be tidied up a little, but briefly testing email signups/login/sessions it seemed to work for my use case (my find user implementation just searched for a user based on email, and an extra tenantId column which I just set to the ctx hostname for testing)

I just see it as a global solution to identify users in any way with one quick solution - a replacement to the user plugin, login by phone number etc

@PodkopovP commented on GitHub (May 17, 2025): > Adding some other related resources I found: > > Discussion with @ping-maxwell about this issue: https://www.answeroverflow.com/m/1362421737203175544 > > PR for a WIP fix by @PodkopovP: https://github.com/better-auth/better-auth/pull/2342 > > I'm currently exploring the following: > 1. Trying the composite email solution posted by @Bekacru but also include `tenantId` and `tenantEmail` to my user schema. > 2. Testing the above PR to see if that works for me. If it does, I will add to it and make another PR to try to push that along. > 3. Look into creating a plugin to solve this. But it looks like the plugin would need the expanded functionality for `findUserByEmail` that the PR addresses anyway. > My PR was more of a proof of functionality I wanted to share rather than a full fledged solution - it can definitely be tidied up a little, but briefly testing email signups/login/sessions it seemed to work for my use case (my find user implementation just searched for a user based on email, and an extra tenantId column which I just set to the ctx hostname for testing) I just see it as a global solution to identify users in any way with one quick solution - a replacement to the user plugin, login by phone number etc
Author
Owner

@Sleepful commented on GitHub (May 17, 2025):

At least for social sign-in it won't matter much, as each domain will have its own client-side session data, and there is no password to worry about.

@Sleepful commented on GitHub (May 17, 2025): At least for `social` sign-in it won't matter much, as each domain will have its own client-side session data, and there is no `password` to worry about.
Author
Owner

@astanciu commented on GitHub (May 26, 2025):

Would also love this functionality. Has anyone gotten this to work in any way?

@astanciu commented on GitHub (May 26, 2025): Would also love this functionality. Has anyone gotten this to work in any way?
Author
Owner

@astanciu commented on GitHub (May 28, 2025):

FYI, I added basic support for tenantId here:
https://github.com/better-auth/better-auth/pull/2819

@astanciu commented on GitHub (May 28, 2025): FYI, I added basic support for `tenantId` here: https://github.com/better-auth/better-auth/pull/2819
Author
Owner

@agarwalvaibhav0211 commented on GitHub (Jun 3, 2025):

We recently implemented multi tenancy using better-auth with separate userspace for each tenant for our systems. While I can't share the exact code, I'll list the approach we followed. We used MongoDB as our database, so the customizations were much easier.

We created a middleware which mapped the request to a tenant(the logic of this would be specific to your implementation, using Origin, domain, path, etc.). Now store this tenantId in a NodeJS AsyncLocalStorage. Run the next middleware under the context.

Next, we copied the MongoDB adapter and made changes to add tenantId to all filters and document insertions.

This way, all the data is scoped to a tenant. This should be scalable to every plugin as far as my understanding goes

I'm not sure if this is the correct or graceful approach. We are currently still testing this and this was more of a PoC since we discovered this excellent library this week.

@agarwalvaibhav0211 commented on GitHub (Jun 3, 2025): We recently implemented multi tenancy using better-auth with separate userspace for each tenant for our systems. While I can't share the exact code, I'll list the approach we followed. We used MongoDB as our database, so the customizations were much easier. We created a middleware which mapped the request to a tenant(the logic of this would be specific to your implementation, using Origin, domain, path, etc.). Now store this tenantId in a NodeJS [AsyncLocalStorage](https://nodejs.org/api/async_context.html#class-asynclocalstorage). Run the next middleware under the context. Next, we copied the [MongoDB adapter](https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/adapters/mongodb-adapter/mongodb-adapter.ts) and made changes to add tenantId to all filters and document insertions. This way, all the data is scoped to a tenant. This should be scalable to every plugin as far as my understanding goes I'm not sure if this is the correct or graceful approach. We are currently still testing this and this was more of a PoC since we discovered this excellent library this week.
Author
Owner

@5ebastianMeier commented on GitHub (Jun 3, 2025):

I like the discussion so far and just wanted to add, that I'd really welcome it if this was an officially supported use case. I'm currently keeping an eye on this library for when it is feature complete so that we can switch a rather big codebase to use better auth. Having this would make the decision way easier, because we could replace existing code instead of combining both implementations.
It might be good to modularize the organization plugin with strategies to keep the overall implementation in line. Of course there could be an additional plugin focused on the additional feature set, but the overlap between the use cases seems quite big.

@5ebastianMeier commented on GitHub (Jun 3, 2025): I like the discussion so far and just wanted to add, that I'd really welcome it if this was an officially supported use case. I'm currently keeping an eye on this library for when it is feature complete so that we can switch a rather big codebase to use better auth. Having this would make the decision way easier, because we could replace existing code instead of combining both implementations. It might be good to modularize the organization plugin with strategies to keep the overall implementation in line. Of course there could be an additional plugin focused on the additional feature set, but the overlap between the use cases seems quite big.
Author
Owner

@PodkopovP commented on GitHub (Jun 5, 2025):

I would say tenants are separate to organisations - there are plenty of use-cases where you'd want multiple tenants, with multiple organisations, with multiple users (i.e. b2b)

@PodkopovP commented on GitHub (Jun 5, 2025): I would say tenants are separate to organisations - there are plenty of use-cases where you'd want multiple tenants, with multiple organisations, with multiple users (i.e. b2b)
Author
Owner

@rinshadkv commented on GitHub (Jul 22, 2025):

any progress on this ?

@rinshadkv commented on GitHub (Jul 22, 2025): any progress on this ?
Author
Owner

@nachthammer commented on GitHub (Sep 10, 2025):

I would also love the idea for this, as I also need to support different users with the same mail (unique non null unique constraint to email and tenant_id).
And I think without the findUserByEmail this will be hard to achieve.

@nachthammer commented on GitHub (Sep 10, 2025): I would also love the idea for this, as I also need to support different users with the same mail (unique non null unique constraint to email and tenant_id). And I think without the `findUserByEmail` this will be hard to achieve.
Author
Owner

@deslunes commented on GitHub (Nov 1, 2025):

Just to inform you guys that I found a random repository using Better-Auth with a custom plugin that aims to provide multi-tenancy.

I didn't test it yet, but it adds a tenantId field to the user table, and allows to use the same email for creating an account on different tenant.

I don't know how other plugins would behave with this. If this is working fine, it could be the base for an official plugin, or a community driven one. I also want to tag the author, in hope they answer, as they might be the best to talk about it. @aashahin

Here's the link of the plugin in the repo: https://github.com/aashahin/Elysia-SaaS-Boilerplate/blob/master/src/packages/tenants/auth.ts

@deslunes commented on GitHub (Nov 1, 2025): Just to inform you guys that I found a random repository using Better-Auth with a custom plugin that aims to provide multi-tenancy. I didn't test it yet, but it adds a tenantId field to the user table, and allows to use the same email for creating an account on different tenant. I don't know how other plugins would behave with this. If this is working fine, it could be the base for an official plugin, or a community driven one. I also want to tag the author, in hope they answer, as they might be the best to talk about it. @aashahin Here's the link of the plugin in the repo: https://github.com/aashahin/Elysia-SaaS-Boilerplate/blob/master/src/packages/tenants/auth.ts
Author
Owner

@better-auth-agent[bot] commented on GitHub (Nov 1, 2025):

Based on what you’re describing, it’s clear that you need to extend Better Auth’s user schema by making tenant-specific fields behave as unique constraints so that the same email can be used in different tenant contexts. The proposed approach has three main components:

  1. Modify the user configuration to allow your additional unique fields (for example, tenantId) to have a unique flag. This means that besides the email address, any field marked as unique must be validated when a user is inserted or updated. For example, your user schema could be updated like this:

  user: {
   additionalFields: {
    tenantId: {
     type: "string",
     required: true,
     unique: true,
     input: true,
    },
   },
  }

  1. Enhance the user lookup functionality by extending the find function. In the sign-up route, instead of checking only for an existing email, the lookup should now factor in any fields marked as unique. In practice, this means revising the findUserByEmail function to accept additional search parameters (e.g. tenantId) and building the query with these extra conditions. This ensures that when a user signs up, the system only rejects the attempt if the combination of email and tenant-specific unique field already exists.

  2. Update your sign-up API route to pass these additional fields when calling the updated findUserByEmail. This way, the API will log and return an error that correctly reflects which unique constraint (e.g. tenantId in addition to email) was violated.

Additional recommendations include testing this new logic thoroughly across your other plugins (such as organization management) to ensure consistency throughout your multi-tenant SaaS architecture. Also, consider checking out community plugins that already tackle multi-tenancy with Better Auth as a starting point for an official or community-maintained solution.

This plan aligns with the discussions and suggestions seen in recent feature proposals and community contributions. It’s a promising approach that you might want to prototype and share with the community for further feedback before turning it into a full-fledged plugin. [1][2]

DiagramDiscordGitHub

Diagram Join Star

@better-auth-agent[bot] commented on GitHub (Nov 1, 2025): Based on what you’re describing, it’s clear that you need to extend Better Auth’s user schema by making tenant-specific fields behave as unique constraints so that the same email can be used in different tenant contexts. The proposed approach has three main components: 1. Modify the user configuration to allow your additional unique fields (for example, tenantId) to have a unique flag. This means that besides the email address, any field marked as unique must be validated when a user is inserted or updated. For example, your user schema could be updated like this:   user: {    additionalFields: {     tenantId: {      type: "string",      required: true,      unique: true,      input: true,     },    },   } 2. Enhance the user lookup functionality by extending the find function. In the sign-up route, instead of checking only for an existing email, the lookup should now factor in any fields marked as unique. In practice, this means revising the findUserByEmail function to accept additional search parameters (e.g. tenantId) and building the query with these extra conditions. This ensures that when a user signs up, the system only rejects the attempt if the combination of email and tenant-specific unique field already exists. 3. Update your sign-up API route to pass these additional fields when calling the updated findUserByEmail. This way, the API will log and return an error that correctly reflects which unique constraint (e.g. tenantId in addition to email) was violated. Additional recommendations include testing this new logic thoroughly across your other plugins (such as organization management) to ensure consistency throughout your multi-tenant SaaS architecture. Also, consider checking out community plugins that already tackle multi-tenancy with Better Auth as a starting point for an official or community-maintained solution. This plan aligns with the discussions and suggestions seen in recent feature proposals and community contributions. It’s a promising approach that you might want to prototype and share with the community for further feedback before turning it into a full-fledged plugin. [[1]](https://github.com/better-auth/better-auth/issues/1248)[[2]](https://github.com/better-auth/better-auth/issues/1557) <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@kgierke commented on GitHub (Nov 18, 2025):

Any news on this? I saw that the PR from @PodkopovP got closed with the comment that a custom findUser will not be supported.

@kgierke commented on GitHub (Nov 18, 2025): Any news on this? I saw that the PR from @PodkopovP got closed with the comment that a custom findUser will not be supported.
Author
Owner

@rboy0509 commented on GitHub (Nov 20, 2025):

How i did this:

On the client side I added:

  fetchOptions:{
    onRequest: async (context) => {
      context.headers.set('tenantId', 'app1');      // Or modify the body
      return context;
    }
  }
import { emailOTPClient } from 'better-auth/client/plugins';
import { createAuthClient } from 'better-auth/react';
export const authClient = createAuthClient({
  /** The base URL of the server (optional if you're using the same domain) */
  baseURL: process.env.NEXT_PUBLIC_API,
  plugins: [emailOTPClient()],
  fetchOptions:{
    onRequest: async (context) => {
      context.headers.set('tenantId', 'app1');      // Or modify the body
      return context;
    }
  }
});

On the server side (using Mongoose)

This can also be done with any ORM.

  • I added tenantId.
  • I made email not unique.
  • I created a compound unique index on { tenantId, email }.
import mongoose, { InferSchemaType, Schema } from "mongoose";

const options = {
  discriminatorKey: "tenantId",
  collection: "users",
  timestamps: true,
};

const userSchema = new Schema(
  {
    tenantId: { type: String, required: true },
    name: { type: String },
    image: { type: String },
    email: { type: String },
    emailVerified: { type: Boolean, default: false },
  },
  options
);

// Create compound unique index on app + email
userSchema.index({ tenantId: 1, email: 1 }, { unique: true });

export const UserModel = mongoose.model("User", userSchema);

// Infer type from schema
export type User = InferSchemaType<typeof userSchema> & { _id: any };

In the auth config:

I used a before hook that overrides the internal adapter for createUser and findUserByEmail.

  hooks: {
    before: createAuthMiddleware(async ctx => {
      overrideAdapterForTenant(ctx);
    }),
  },
const overrideAdapterForTenant = (ctx: any) => {
  
  const tenantId = ctx.request?.headers.get('tenantId');

  // Store tenantId in context for later use
  (ctx.context as any).tenantId = tenantId;

  //Override createUser to use mongoose model
  ctx.context.internalAdapter.createUser = async user => {
    const createdUser = await UserModel.create({
      tenantId,
      ...user,
    });

    const userObj = createdUser.toObject();

    // Transform MongoDB document to better-auth format
    return {
      ...userObj,
      id: userObj._id?.toString(),
      _id: undefined,
    } as any;
  };

  // Override findUserByEmail to include tenantId filtering
  ctx.context.internalAdapter.findUserByEmail = async (
    email: string,
    options?: { includeAccounts: boolean },
  ) => {
    const user = await UserModel.findOne({ tenantId, email }).lean();
    if (!user) return null;

    // Transform MongoDB document to better-auth format
    const transformedUser = {
      ...user,
      id: user._id?.toString(),
      _id: undefined,
    };

    return {
      user: transformedUser as any,
      accounts: [],
    };
  };
};
@rboy0509 commented on GitHub (Nov 20, 2025): How i did this: On the client side I added: ``` fetchOptions:{ onRequest: async (context) => { context.headers.set('tenantId', 'app1'); // Or modify the body return context; } } ``` ``` import { emailOTPClient } from 'better-auth/client/plugins'; import { createAuthClient } from 'better-auth/react'; export const authClient = createAuthClient({ /** The base URL of the server (optional if you're using the same domain) */ baseURL: process.env.NEXT_PUBLIC_API, plugins: [emailOTPClient()], fetchOptions:{ onRequest: async (context) => { context.headers.set('tenantId', 'app1'); // Or modify the body return context; } } }); ``` On the server side (using Mongoose) This can also be done with any ORM. - I added tenantId. - - I made email not unique. - - I created a compound unique index on { tenantId, email }. ``` import mongoose, { InferSchemaType, Schema } from "mongoose"; const options = { discriminatorKey: "tenantId", collection: "users", timestamps: true, }; const userSchema = new Schema( { tenantId: { type: String, required: true }, name: { type: String }, image: { type: String }, email: { type: String }, emailVerified: { type: Boolean, default: false }, }, options ); // Create compound unique index on app + email userSchema.index({ tenantId: 1, email: 1 }, { unique: true }); export const UserModel = mongoose.model("User", userSchema); // Infer type from schema export type User = InferSchemaType<typeof userSchema> & { _id: any }; ``` In the auth config: I used a before hook that overrides the internal adapter for createUser and findUserByEmail. ``` hooks: { before: createAuthMiddleware(async ctx => { overrideAdapterForTenant(ctx); }), }, ``` ``` const overrideAdapterForTenant = (ctx: any) => { const tenantId = ctx.request?.headers.get('tenantId'); // Store tenantId in context for later use (ctx.context as any).tenantId = tenantId; //Override createUser to use mongoose model ctx.context.internalAdapter.createUser = async user => { const createdUser = await UserModel.create({ tenantId, ...user, }); const userObj = createdUser.toObject(); // Transform MongoDB document to better-auth format return { ...userObj, id: userObj._id?.toString(), _id: undefined, } as any; }; // Override findUserByEmail to include tenantId filtering ctx.context.internalAdapter.findUserByEmail = async ( email: string, options?: { includeAccounts: boolean }, ) => { const user = await UserModel.findOne({ tenantId, email }).lean(); if (!user) return null; // Transform MongoDB document to better-auth format const transformedUser = { ...user, id: user._id?.toString(), _id: undefined, }; return { user: transformedUser as any, accounts: [], }; }; }; ```
Author
Owner

@I-Want-ToBelieve commented on GitHub (Nov 28, 2025):

I think all tables should have a tenant ID column with a default value.

This allows compatibility for systems that don’t need multi-tenant support now but may need it in the future.

For compatibility and extensibility, treating a single value as an array with just one element is a common design pattern: element → [element].

@I-Want-ToBelieve commented on GitHub (Nov 28, 2025): I think all tables should have a tenant ID column with a default value. This allows compatibility for systems that don’t need multi-tenant support now but may need it in the future. For compatibility and extensibility, treating a single value as an array with just one element is a common design pattern: `element → [element]`.
Author
Owner

@dosubot[bot] commented on GitHub (Feb 27, 2026):

Hi, @robskinney. I'm Dosu, and I'm helping the better-auth team manage their backlog and am marking this issue as stale.

Issue Summary:

  • You requested multi-tenant support in Better Auth, specifically unique constraints on user fields like tenantId.
  • Community members discussed various workarounds including custom internalAdapter methods, email prefixing, and middleware solutions.
  • A proof of concept PR was submitted but closed due to limitations with custom findUser support.
  • Several users shared practical approaches involving database compound unique indexes and middleware.
  • There remains ongoing interest in an official or community plugin to support multi-tenancy.

Next Steps:

  • Please let me know if this issue is still relevant to the latest version of better-auth by commenting here to keep the discussion open.
  • Otherwise, I will automatically close this issue in 7 days.

Thank you for your understanding and contribution!

@dosubot[bot] commented on GitHub (Feb 27, 2026): Hi, @robskinney. I'm [Dosu](https://dosu.dev), and I'm helping the better-auth team manage their backlog and am marking this issue as stale. **Issue Summary:** - You requested multi-tenant support in Better Auth, specifically unique constraints on user fields like tenantId. - Community members discussed various workarounds including custom internalAdapter methods, email prefixing, and middleware solutions. - A proof of concept PR was submitted but closed due to limitations with custom findUser support. - Several users shared practical approaches involving database compound unique indexes and middleware. - There remains ongoing interest in an official or community plugin to support multi-tenancy. **Next Steps:** - Please let me know if this issue is still relevant to the latest version of better-auth by commenting here to keep the discussion open. - Otherwise, I will automatically close this issue in 7 days. Thank you for your understanding and contribution!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#575