Create organization on user sign-up #920

Closed
opened 2026-03-13 08:09:54 -05:00 by GiteaMirror · 17 comments
Owner

Originally created by @CodeWithShreyans on GitHub (Mar 26, 2025).

I want to create an org whenever a user signs up.

Is it possible to call auth().api.createOrganization() inside the hook?

		hooks: {
			after: createAuthMiddleware(async (ctx) => {
				if(ctx.path.startsWith("/sign-up")){
					const newSession = ctx.context.newSession;
					if(newSession){
						// Create org here
					}
				}
			}),
		}
Originally created by @CodeWithShreyans on GitHub (Mar 26, 2025). I want to create an org whenever a user signs up. Is it possible to call `auth().api.createOrganization()` inside the hook? ```js hooks: { after: createAuthMiddleware(async (ctx) => { if(ctx.path.startsWith("/sign-up")){ const newSession = ctx.context.newSession; if(newSession){ // Create org here } } }), } ```
Author
Owner

@AlexanderHott commented on GitHub (Mar 28, 2025):

this is how I'm doing it, although it could be made much better

  databaseHooks: {
    user: {
      create: {
        async after(user) {
          // FIXME: make this better
          const slug =
            user.name.toLowerCase().replaceAll(" ", "-") +
            "-" +
            Math.floor(Math.random() * 10_000).toString();
          const org = await auth.api.createOrganization({
            body: {
              name: `${user.name}'s Organization`,
              slug,
              userId: user.id,
            },
          });
          if (org === null) {
            console.error("failed to create organization on account creation");
            return;
          }

          await auth.api.setActiveOrganization({
            body: { organizationId: org.id },
          });
        },
      },
    },
@AlexanderHott commented on GitHub (Mar 28, 2025): this is how I'm doing it, although it could be made much better ```ts databaseHooks: { user: { create: { async after(user) { // FIXME: make this better const slug = user.name.toLowerCase().replaceAll(" ", "-") + "-" + Math.floor(Math.random() * 10_000).toString(); const org = await auth.api.createOrganization({ body: { name: `${user.name}'s Organization`, slug, userId: user.id, }, }); if (org === null) { console.error("failed to create organization on account creation"); return; } await auth.api.setActiveOrganization({ body: { organizationId: org.id }, }); }, }, }, ```
Author
Owner

@CodeWithShreyans commented on GitHub (Mar 31, 2025):

@Bekacru opinion on this?

@CodeWithShreyans commented on GitHub (Mar 31, 2025): @Bekacru opinion on this?
Author
Owner

@matthewlynch commented on GitHub (Apr 1, 2025):

The above code works for me but I consistently see an error logged when I call createOrganization:

[APIError] {
  status: 'UNAUTHORIZED',
  body: undefined,
  headers: {},
  statusCode: 401
}
Image
@matthewlynch commented on GitHub (Apr 1, 2025): The above code works for me but I consistently see an error logged when I call `createOrganization`: ``` [APIError] { status: 'UNAUTHORIZED', body: undefined, headers: {}, statusCode: 401 } ``` <img width="2036" alt="Image" src="https://github.com/user-attachments/assets/4fc59287-5c3c-4714-8ac9-445eb6e23bcb" />
Author
Owner

@tehnrd commented on GitHub (Apr 15, 2025):

I would love to get guidance on correctly handling this from better-auth maintainers/experts. I'm building a B2B app, and all users will be part of an organization, so every user that signs up and logs in needs to be associated with an organization.

It may simply be a documentation update to use hooks, as business logic may be required when creating an organization, but if there is a gap in functionality, that would be noteworthy too.

@tehnrd commented on GitHub (Apr 15, 2025): I would love to get guidance on correctly handling this from better-auth maintainers/experts. I'm building a B2B app, and all users will be part of an organization, so every user that signs up and logs in needs to be associated with an organization. It may simply be a documentation update to use hooks, as business logic may be required when creating an organization, but if there is a gap in functionality, that would be noteworthy too.
Author
Owner

@tehnrd commented on GitHub (Apr 16, 2025):

I didn't want to create an org for every user, so what I did was have some logic to determine if I should prompt the user to create an org after signing in.

https://github.com/better-auth/better-auth/issues/1023#issuecomment-2809527284

@tehnrd commented on GitHub (Apr 16, 2025): I didn't want to create an org for every user, so what I did was have some logic to determine if I should prompt the user to create an org after signing in. https://github.com/better-auth/better-auth/issues/1023#issuecomment-2809527284
Author
Owner

@k3dom commented on GitHub (May 1, 2025):

The above code works for me but I consistently see an error logged when I call createOrganization:

[APIError] {
  status: 'UNAUTHORIZED',
  body: undefined,
  headers: {},
  statusCode: 401
}
Image

Facing this same issue. Creating the organization works but a 401 error gets logged.

@k3dom commented on GitHub (May 1, 2025): > The above code works for me but I consistently see an error logged when I call `createOrganization`: > > ``` > [APIError] { > status: 'UNAUTHORIZED', > body: undefined, > headers: {}, > statusCode: 401 > } > ``` > > <img alt="Image" width="2000" src="https://private-user-images.githubusercontent.com/3437234/429189915-4fc59287-5c3c-4714-8ac9-445eb6e23bcb.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDYxMjcwMDksIm5iZiI6MTc0NjEyNjcwOSwicGF0aCI6Ii8zNDM3MjM0LzQyOTE4OTkxNS00ZmM1OTI4Ny01YzNjLTQ3MTQtOGFjOS00NDVlYjZlMjNiY2IucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDUwMSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTA1MDFUMTkxMTQ5WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ZWQ2ODRjM2NjNzQ0NWQxMWZmYzE0NmQyYjYzYTdiYTNlY2E5NGQzNzI0NTlhMmVmNjYwYTVmNDRiMGNhMGI4YiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ._U2w7Njs8kVdbBcLOA5o1vj3sSz8xY_-ImrfCpsWHeM"> Facing this same issue. Creating the organization works but a 401 error gets logged.
Author
Owner

@k3dom commented on GitHub (May 2, 2025):

This is the code that ended up working for me. This creates a default organization for each user upon creation and also activates the (first) organization upon session creation. The main "issue" here is the probable name clash with the slug. In case of a clash I delete the user database entry so he can try signing up again, with the hope that the new random string does not clash.

export const auth = betterAuth({
  // ...,
  plugins: [organization()],
  databaseHooks: {
    user: {
      create: {
        after: async (user) => {
          await createDefaultOrganization(user)
        },
      },
    },
    session: {
      create: {
        before: async (session) => {
          return await setActiveOrganization(session)
        },
      },
    },
  },
  // ...,
})

async function createDefaultOrganization(
  user: typeof auth.$Infer.Session.user
) {
  try {
    const randomString = ulid().slice(0, 8)
    await auth.api.createOrganization({
      body: {
        userId: user.id,
        name: `${user.name}'s Organization`,
        slug: `${user.name
          .split(' ')
          .map((n) => n[0])
          .join('')
          .toLowerCase()}-org-${randomString}`,
      },
    })
  } catch (err) {
    db.delete($user).where(eq($user.id, user.id))
    throw err
  }
}

async function setActiveOrganization(session: { userId: string }) {
  const firstOrg = await db.query.$member.findFirst({
    where: ($member, { eq }) => eq($member.userId, session.userId),
  })

  return {
    data: {
      ...session,
      activeOrganizationId: firstOrg?.organizationId,
    },
  }
}
@k3dom commented on GitHub (May 2, 2025): This is the code that ended up working for me. This creates a default organization for each user upon creation and also activates the (first) organization upon session creation. The main "issue" here is the probable name clash with the `slug`. In case of a clash I delete the user database entry so he can try signing up again, with the hope that the new random string does not clash. ```ts export const auth = betterAuth({ // ..., plugins: [organization()], databaseHooks: { user: { create: { after: async (user) => { await createDefaultOrganization(user) }, }, }, session: { create: { before: async (session) => { return await setActiveOrganization(session) }, }, }, }, // ..., }) async function createDefaultOrganization( user: typeof auth.$Infer.Session.user ) { try { const randomString = ulid().slice(0, 8) await auth.api.createOrganization({ body: { userId: user.id, name: `${user.name}'s Organization`, slug: `${user.name .split(' ') .map((n) => n[0]) .join('') .toLowerCase()}-org-${randomString}`, }, }) } catch (err) { db.delete($user).where(eq($user.id, user.id)) throw err } } async function setActiveOrganization(session: { userId: string }) { const firstOrg = await db.query.$member.findFirst({ where: ($member, { eq }) => eq($member.userId, session.userId), }) return { data: { ...session, activeOrganizationId: firstOrg?.organizationId, }, } } ```
Author
Owner

@iamtekeste commented on GitHub (May 3, 2025):

@kedom1337 your approach looks good to me but why is slug an "issue"? isn't the random string generated by ulid random enough?

@iamtekeste commented on GitHub (May 3, 2025): @kedom1337 your approach looks good to me but why is slug an "issue"? isn't the random string generated by ulid random enough?
Author
Owner

@k3dom commented on GitHub (May 3, 2025):

@kedom1337 your approach looks good to me but why is slug an "issue"? isn't the random string generated by ulid random enough?

Well as you can see I only take the first 8 characters and not the full ULID. This obviously reduces entropy but should probably not be a significant problem. You could improve this further by handling the error and retrying with a new ULID maybe like 5 times till you don't get a clash.

@k3dom commented on GitHub (May 3, 2025): > [@kedom1337](https://github.com/kedom1337) your approach looks good to me but why is slug an "issue"? isn't the random string generated by ulid random enough? Well as you can see I only take the first 8 characters and not the full ULID. This obviously reduces entropy but should probably not be a significant problem. You could improve this further by handling the error and retrying with a new ULID maybe like 5 times till you don't get a clash.
Author
Owner

@zpg6 commented on GitHub (May 4, 2025):

Without going out of scope with this comment - how would you all use this new feature to align with the referenceId param in the stripe plugin that allows billing by-organization or by-user? Assuming I wanted to offer plans like Pro, Max, and Teams (with seats) style billing...

Possible Usage

I'm wondering if I should force everyone to have a "Personal Organization" that they can't delete, which would simplify things to just by-organization billing with 1 seat (Pro, Max) or 2+ seats (Teams).

  • Pro/Max: only a 1seat Personal org where only they can access it
  • Teams: they can start on their Personal org, grow it, rename it if they want, and invite people to it over time.
  • Teams: they can start with a combined org and invite people from the start

Thanks for your thoughts on how you might be planning to use this feature

Takeaway

For configuring a defaultOrganization plugin if that's how this goes:

  1. Consider allowing to supply a function for createOrganizationDetails({ user, ... }) instead of assuming things like ${user.name}'s Organization.
  2. Consider allowing to supply a boolean allowDeletion of a default org so we can potentially prevent "organization-less users" in our db and "ghost customers" on Stripe and other headaches. You may then need to keep track of whether it was initially created as a default.
@zpg6 commented on GitHub (May 4, 2025): Without going out of scope with this comment - how would you all use this new feature to align with the `referenceId` param in the stripe plugin that allows billing _by-organization_ or _by-user_? Assuming I wanted to offer plans like Pro, Max, and Teams (with seats) style billing... ## Possible Usage I'm wondering if I should force everyone to have a "Personal Organization" that they can't delete, which would simplify things to just _by-organization_ billing with 1 seat (Pro, Max) or 2+ seats (Teams). - Pro/Max: only a 1seat Personal org where only they can access it - Teams: they can start on their Personal org, grow it, rename it if they want, and invite people to it over time. - Teams: they can start with a combined org and invite people from the start Thanks for your thoughts on how you might be planning to use this feature ## Takeaway For configuring a `defaultOrganization` plugin if that's how this goes: 1. Consider allowing to supply a function for `createOrganizationDetails({ user, ... })` instead of assuming things like `${user.name}'s Organization`. 2. Consider allowing to supply a boolean `allowDeletion` of a default org so we can potentially prevent "organization-less users" in our db and "ghost customers" on Stripe and other headaches. You may then need to keep track of whether it was initially created as a default.
Author
Owner

@iamtekeste commented on GitHub (May 5, 2025):

I'm wondering if I should force everyone to have a "Personal Organization" that they can't delete

This is exactly how i plan to do it as well

@iamtekeste commented on GitHub (May 5, 2025): > I'm wondering if I should force everyone to have a "Personal Organization" that they can't delete This is exactly how i plan to do it as well
Author
Owner

@Kinfe123 commented on GitHub (May 31, 2025):

@iamtekeste you can create an org right when the user / session created

@Kinfe123 commented on GitHub (May 31, 2025): @iamtekeste you can create an org right when the user / session created
Author
Owner

@dosubot[bot] commented on GitHub (Sep 12, 2025):

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

Issue Summary:

  • You inquired about automatically creating an organization during user sign-up using auth().api.createOrganization() in the after hook.
  • Contributors shared working implementations via database hooks, but some users encountered recurring 401 UNAUTHORIZED errors when calling createOrganization.
  • The discussion included best practices for ensuring slug uniqueness and managing default organizations for billing purposes.
  • There was also debate on whether to prevent deletion of default "Personal Organizations" to simplify billing and user management.
  • Maintainers and community members are considering documentation improvements and business logic refinements related to this feature.

Next Steps:

  • Please let me know if this issue is still relevant with the latest version of better-auth by commenting here to keep the discussion open.
  • If I don’t hear back within 7 days, I will automatically close this issue to keep the backlog manageable.

Thanks for your understanding and contribution!

@dosubot[bot] commented on GitHub (Sep 12, 2025): Hi, @CodeWithShreyans. 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 inquired about automatically creating an organization during user sign-up using `auth().api.createOrganization()` in the after hook. - Contributors shared working implementations via database hooks, but some users encountered recurring 401 UNAUTHORIZED errors when calling `createOrganization`. - The discussion included best practices for ensuring slug uniqueness and managing default organizations for billing purposes. - There was also debate on whether to prevent deletion of default "Personal Organizations" to simplify billing and user management. - Maintainers and community members are considering documentation improvements and business logic refinements related to this feature. **Next Steps:** - Please let me know if this issue is still relevant with the latest version of better-auth by commenting here to keep the discussion open. - If I don’t hear back within 7 days, I will automatically close this issue to keep the backlog manageable. Thanks for your understanding and contribution!
Author
Owner

@enisze commented on GitHub (Sep 27, 2025):

Hi, I am experiencing exactly the same issue with the 401 UNAUTHORIZED. Although I am doing it within a server action

const authResult = await auth.api.signUpEmail({
					body: {
						email,
						password,
					},
					headers: await headers(),
				});

				const signedIn = await auth.api.signInEmail({
					headers: await headers(),
					body: { email, password },
				});


				const session = await auth.api.getSession({
					headers: await headers(),
				});

-> The session is undefined here though, which is probably why I cannot create the organization and get a 401 UNAUTHORIZED, but I am unsure
				logger.info(JSON.stringify(session?.user));

				const organization = await auth.api
					.createOrganization({
						body: {
							name: companyName,
							businessAddressId: address.id,
							numberOfProperties,
							numberOfUnits,
							userId: authResult.user.id,
							slug: companyName.toLowerCase().replace(/ /g, "-"),
						},
						headers: await headers(),
					})

not sure if it has to do something with the session here.

EDIT: Well seems like AI helped me out:

#4945

@enisze commented on GitHub (Sep 27, 2025): Hi, I am experiencing exactly the same issue with the 401 UNAUTHORIZED. Although I am doing it within a server action ```ts const authResult = await auth.api.signUpEmail({ body: { email, password, }, headers: await headers(), }); const signedIn = await auth.api.signInEmail({ headers: await headers(), body: { email, password }, }); const session = await auth.api.getSession({ headers: await headers(), }); -> The session is undefined here though, which is probably why I cannot create the organization and get a 401 UNAUTHORIZED, but I am unsure logger.info(JSON.stringify(session?.user)); const organization = await auth.api .createOrganization({ body: { name: companyName, businessAddressId: address.id, numberOfProperties, numberOfUnits, userId: authResult.user.id, slug: companyName.toLowerCase().replace(/ /g, "-"), }, headers: await headers(), }) ``` not sure if it has to do something with the session here. EDIT: Well seems like AI helped me out: #4945
Author
Owner

@austinm911 commented on GitHub (Oct 18, 2025):

It's a bit confusing that default organization creation isn't implemented (https://github.com/better-auth/better-auth/pull/4600) but there is a type in the teams object defaultTeam that creates a team on org creation. I ended up hunting around trying to figure out where the defaultOrg flag was or if I was not finding it, until I landed here.

@austinm911 commented on GitHub (Oct 18, 2025): It's a bit confusing that default organization creation isn't implemented (https://github.com/better-auth/better-auth/pull/4600) but there is a type in the teams object `defaultTeam` that creates a team on org creation. I ended up hunting around trying to figure out where the `defaultOrg` flag was or if I was not finding it, until I landed here.
Author
Owner

@dosubot[bot] commented on GitHub (Jan 22, 2026):

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

Issue Summary:

  • You asked about automatically creating an organization during user sign-up using auth().api.createOrganization() in an after-sign-up hook.
  • Contributors shared working code examples using database hooks to achieve this.
  • Some users reported recurring 401 UNAUTHORIZED errors when calling createOrganization, likely due to session or authorization issues.
  • The discussion included best practices for slug uniqueness, managing default organizations for billing, and handling deletion of default "Personal Organizations."
  • Maintainers are considering documentation improvements and clarifications around this feature.

Next Steps:

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

Thanks for your understanding and contribution!

@dosubot[bot] commented on GitHub (Jan 22, 2026): Hi, @CodeWithShreyans. 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 asked about automatically creating an organization during user sign-up using `auth().api.createOrganization()` in an after-sign-up hook. - Contributors shared working code examples using database hooks to achieve this. - Some users reported recurring 401 UNAUTHORIZED errors when calling `createOrganization`, likely due to session or authorization issues. - The discussion included best practices for slug uniqueness, managing default organizations for billing, and handling deletion of default "Personal Organizations." - Maintainers are considering documentation improvements and clarifications around this feature. **Next Steps:** - Please let me know if this issue is still relevant with the latest version of better-auth by commenting here to keep the discussion open. - Otherwise, this issue will be automatically closed in 7 days. Thanks for your understanding and contribution!
Author
Owner

@leeteral commented on GitHub (Mar 6, 2026):

bump

@leeteral commented on GitHub (Mar 6, 2026): bump
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#920