Cannot set active organization after accepting invitation — no support for setting active organization manually #1522

Closed
opened 2026-03-13 08:45:03 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @brunowego on GitHub (Jul 18, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. We use Better Auth in a Next.js app with the App Router.
  2. The app loads the current workspaceId from the URL, and we don’t persist organizationId in the session.
  3. We have a custom invite acceptance flow using the following snippet:
'use client'

import { useRouter } from 'next/navigation'

import { organization } from '@acme/auth/client'
import type { Invitation } from '@acme/auth'
import Button from '@acme/ui/components/button'
import toast from '@acme/ui/lib/toast'

type AcceptButtonProps = {
  invitation: Invitation
}

export default function AcceptButton({ invitation }: AcceptButtonProps) {
  const { push } = useRouter()

  const handleAccept = async () => {
    await organization.acceptInvitation({
      invitationId: invitation.id,
      fetchOptions: {
        async onSuccess() {
          // await organization.setActive({
          //   organizationId: invitation.organizationId,
          // })

          toast.success('Invitation accepted successfully!')

          push('/')
        },
      },
    })
  }

  return (
    <Button className='cursor-pointer' onClick={handleAccept}>
      Accept Invitation
    </Button>
  )
}
  1. The user accepts the invitation successfully, but with 500 error.
  2. We cannot set the active organization manually using organization.setActive — the method appears to be unavailable or unsupported in the current flow.

Current vs. Expected behavior

Expected:
After accepting an invitation, the user should be added to the organization and redirected to the homepage. From there, the organization should appear in the list of accessible organizations, and the user should be able to continue normally under the new context.

Actual:
Although the user is successfully added to the organization (confirmed by checking the homepage afterward), the organization.acceptInvitation method returns a 500 Internal Server Error. This interrupts the flow and prevents immediate redirection, despite the invitation having been processed correctly.

What version of Better Auth are you using?

1.2.12

Provide environment information

- OS: macOS 14.5
- Browser: Chrome 125
- Framework: Next.js (App Router, Client Components)

Which area(s) are affected? (Select all that apply)

Client

Auth config (if applicable)

import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { admin, organization } from 'better-auth/plugins'
import { v4 as uuidv4 } from 'uuid'
import { type User, type BetterAuthOptions, betterAuth } from 'better-auth'
import { cache } from 'react'
import { headers } from 'next/headers'

import db from '@acme/db'
import { sendEmail, EmailTemplate } from '@acme/email'

import senv from './lib/env/server'
import penv from './lib/env/client'
import { ac, roles } from './permissions'

export const config = {
  database: drizzleAdapter(db, {
    provider: 'pg',
    usePlural: true,
  }),
  socialProviders: {
    ...(senv.GOOGLE_CLIENT_ID && senv.GOOGLE_CLIENT_SECRET
      ? {
          google: {
            clientId: senv.GOOGLE_CLIENT_ID,
            clientSecret: senv.GOOGLE_CLIENT_SECRET,
            disableSignUp: senv.DISABLE_SIGNUP,
            mapProfileToUser: (profile) => {
              return {
                ...profile,
                firstName: profile.given_name,
                lastName: profile.family_name,
                image: profile.picture?.replace('=s96-c', '=s256-c'),
              }
            },
          },
        }
      : {}),
  },
  plugins: [
    admin({
      defaultRole: 'user',
      adminRoles: ['admin'],
    }),
    organization({
      ac,
      roles,
      sendInvitationEmail: async ({
        email,
        organization,
        inviter,
        invitation,
      }) => {
        await sendEmail(email, EmailTemplate.MemberInviteEmail, {
          workspaceName: organization.name,
          invitedByName: inviter.user.name,
          invitedByEmail: inviter.user.email,
          name: email,
          inviteLink: `${penv.NEXT_PUBLIC_APP_URL}/sign-in?redirectTo=/accept-invitation/${invitation.id}`,
        })
      },
    }),
  ],
  user: {
    additionalFields: {
      firstName: {
        type: 'string',
        fieldName: 'firstName',
      },
      lastName: {
        type: 'string',
        required: false,
        fieldName: 'lastName',
      },
      theme: {
        type: 'string',
        required: false,
        fieldName: 'theme',
      },
      role: {
        type: 'string',
        required: false,
        fieldName: 'role',
      },
    },
  },
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    updateAge: 60 * 60 * 24, // 1 day
  },
  advanced: {
    cookiePrefix: 'acme',
    database: {
      generateId: () => uuidv4(),
    },
  },
  databaseHooks: {
    user: {
      create: {
        before: async (user) => {
          if (user.email.endsWith('@acme.com')) {
            return {
              data: {
                ...user,
                role: 'admin',
              },
            }
          }

          return {
            data: {
              ...user,
            },
          }
        },
        after: async (user) => {
          const { email, role } = user as User & { role: string }

          if (role !== 'admin') {
            return
          }

          try {
            await sendEmail(email, EmailTemplate.StaffCreatedEmail, {
              email,
              signInLink: `${penv.NEXT_PUBLIC_APP_URL}/sign-in`,
            })
          } catch (error) {
            console.error('Failed to send staff creation email:', error)
          }
        },
      },
    },
  },
} satisfies BetterAuthOptions

const auth = betterAuth(config)

const getCurrentSession = cache(
  async () => await auth.api.getSession({ headers: await headers() }),
)

export { auth as default, getCurrentSession }

Additional context

  • We rely entirely on workspaceId in the URL for context.
  • We don’t persist organizationId in the session, so there’s no implicit active org.
  • We need to be able to manually set the organization context immediately after accepting an invitation.
  • This is reproducible both locally and in production.
  • Adding support for organization.setActive (or exposing it clearly) would solve the issue.
Originally created by @brunowego on GitHub (Jul 18, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. We use Better Auth in a Next.js app with the App Router. 2. The app loads the current workspaceId from the URL, and we don’t persist organizationId in the session. 3. We have a custom invite acceptance flow using the following snippet: ```tsx 'use client' import { useRouter } from 'next/navigation' import { organization } from '@acme/auth/client' import type { Invitation } from '@acme/auth' import Button from '@acme/ui/components/button' import toast from '@acme/ui/lib/toast' type AcceptButtonProps = { invitation: Invitation } export default function AcceptButton({ invitation }: AcceptButtonProps) { const { push } = useRouter() const handleAccept = async () => { await organization.acceptInvitation({ invitationId: invitation.id, fetchOptions: { async onSuccess() { // await organization.setActive({ // organizationId: invitation.organizationId, // }) toast.success('Invitation accepted successfully!') push('/') }, }, }) } return ( <Button className='cursor-pointer' onClick={handleAccept}> Accept Invitation </Button> ) } ``` 4. The user accepts the invitation successfully, but with 500 error. 5. We cannot set the active organization manually using organization.setActive — the method appears to be unavailable or unsupported in the current flow. ### Current vs. Expected behavior Expected: After accepting an invitation, the user should be added to the organization and redirected to the homepage. From there, the organization should appear in the list of accessible organizations, and the user should be able to continue normally under the new context. Actual: Although the user is successfully added to the organization (confirmed by checking the homepage afterward), the organization.acceptInvitation method returns a 500 Internal Server Error. This interrupts the flow and prevents immediate redirection, despite the invitation having been processed correctly. ### What version of Better Auth are you using? 1.2.12 ### Provide environment information ```bash - OS: macOS 14.5 - Browser: Chrome 125 - Framework: Next.js (App Router, Client Components) ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript import { drizzleAdapter } from 'better-auth/adapters/drizzle' import { admin, organization } from 'better-auth/plugins' import { v4 as uuidv4 } from 'uuid' import { type User, type BetterAuthOptions, betterAuth } from 'better-auth' import { cache } from 'react' import { headers } from 'next/headers' import db from '@acme/db' import { sendEmail, EmailTemplate } from '@acme/email' import senv from './lib/env/server' import penv from './lib/env/client' import { ac, roles } from './permissions' export const config = { database: drizzleAdapter(db, { provider: 'pg', usePlural: true, }), socialProviders: { ...(senv.GOOGLE_CLIENT_ID && senv.GOOGLE_CLIENT_SECRET ? { google: { clientId: senv.GOOGLE_CLIENT_ID, clientSecret: senv.GOOGLE_CLIENT_SECRET, disableSignUp: senv.DISABLE_SIGNUP, mapProfileToUser: (profile) => { return { ...profile, firstName: profile.given_name, lastName: profile.family_name, image: profile.picture?.replace('=s96-c', '=s256-c'), } }, }, } : {}), }, plugins: [ admin({ defaultRole: 'user', adminRoles: ['admin'], }), organization({ ac, roles, sendInvitationEmail: async ({ email, organization, inviter, invitation, }) => { await sendEmail(email, EmailTemplate.MemberInviteEmail, { workspaceName: organization.name, invitedByName: inviter.user.name, invitedByEmail: inviter.user.email, name: email, inviteLink: `${penv.NEXT_PUBLIC_APP_URL}/sign-in?redirectTo=/accept-invitation/${invitation.id}`, }) }, }), ], user: { additionalFields: { firstName: { type: 'string', fieldName: 'firstName', }, lastName: { type: 'string', required: false, fieldName: 'lastName', }, theme: { type: 'string', required: false, fieldName: 'theme', }, role: { type: 'string', required: false, fieldName: 'role', }, }, }, session: { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24, // 1 day }, advanced: { cookiePrefix: 'acme', database: { generateId: () => uuidv4(), }, }, databaseHooks: { user: { create: { before: async (user) => { if (user.email.endsWith('@acme.com')) { return { data: { ...user, role: 'admin', }, } } return { data: { ...user, }, } }, after: async (user) => { const { email, role } = user as User & { role: string } if (role !== 'admin') { return } try { await sendEmail(email, EmailTemplate.StaffCreatedEmail, { email, signInLink: `${penv.NEXT_PUBLIC_APP_URL}/sign-in`, }) } catch (error) { console.error('Failed to send staff creation email:', error) } }, }, }, }, } satisfies BetterAuthOptions const auth = betterAuth(config) const getCurrentSession = cache( async () => await auth.api.getSession({ headers: await headers() }), ) export { auth as default, getCurrentSession } ``` ### Additional context - We rely entirely on workspaceId in the URL for context. - We don’t persist organizationId in the session, so there’s no implicit active org. - We need to be able to manually set the organization context immediately after accepting an invitation. - This is reproducible both locally and in production. - Adding support for organization.setActive (or exposing it clearly) would solve the issue.
Author
Owner

@dosubot[bot] commented on GitHub (Jul 18, 2025):

This is a known architectural limitation in Better Auth v1.2.12: the organization context is always determined by the session or cookies, not by a manually-passed organizationId, and there is no documented or exposed organization.setActive method in the client API. This means that in setups like yours—where org context is managed via URL and not persisted in the session—there’s no supported way to manually set the active organization after accepting an invitation. The backend expects to update the session’s activeOrganizationId after invitation acceptance, and if your flow doesn’t use session-based org context, this step fails or is a no-op, which is likely why you’re seeing a 500 error even though the membership is updated correctly (source).

As a partial workaround, you can use getFullOrganization with the org slug or id to fetch organization/member info, but this does not set the active org context for the session or client—so it won’t fix context switching or the error (source). There’s currently no alternative for client-only org context management; exposing organization.setActive in the client API is a known gap for these use cases.

If you want more technical detail or want to dig into error logs, let me know!

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

@dosubot[bot] commented on GitHub (Jul 18, 2025): <!-- Answer --> This is a known architectural limitation in Better Auth v1.2.12: the organization context is always determined by the session or cookies, not by a manually-passed organizationId, and there is no documented or exposed organization.setActive method in the client API. This means that in setups like yours—where org context is managed via URL and not persisted in the session—there’s no supported way to manually set the active organization after accepting an invitation. The backend expects to update the session’s activeOrganizationId after invitation acceptance, and if your flow doesn’t use session-based org context, this step fails or is a no-op, which is likely why you’re seeing a 500 error even though the membership is updated correctly ([source](https://github.com/better-auth/better-auth/issues/3414)). As a partial workaround, you can use getFullOrganization with the org slug or id to fetch organization/member info, but this does not set the active org context for the session or client—so it won’t fix context switching or the error ([source](https://github.com/better-auth/better-auth/issues/3414)). There’s currently no alternative for client-only org context management; exposing organization.setActive in the client API is a known gap for these use cases. If you want more technical detail or want to dig into error logs, let me know! <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/c9987b59-b3e3-415b-8c0b-5713a89ad025?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/c9987b59-b3e3-415b-8c0b-5713a89ad025?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/c9987b59-b3e3-415b-8c0b-5713a89ad025?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/c9987b59-b3e3-415b-8c0b-5713a89ad025?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/c9987b59-b3e3-415b-8c0b-5713a89ad025?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/c9987b59-b3e3-415b-8c0b-5713a89ad025?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/c9987b59-b3e3-415b-8c0b-5713a89ad025?feedback_type=other)</sup>&nbsp;&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3452)
Author
Owner

@Bekacru commented on GitHub (Jul 24, 2025):

first setActive exists as a method on the client

client.organization.setActive({
	organizationId: "org_123",
});

we do set the active org after accepting the invite. If it’s returning a 500, check your server logs and let me know, as that gives us more context

@Bekacru commented on GitHub (Jul 24, 2025): first `setActive` exists as a method on the client ```ts client.organization.setActive({ organizationId: "org_123", }); ``` we do set the active org after accepting the invite. If it’s returning a 500, check your server logs and let me know, as that gives us more context
Author
Owner

@frectonz commented on GitHub (Sep 4, 2025):

@brunowego Setting the active organization is supported in the latest version of better auth. You can read more about it here.

Let me know if you are still facing a problem with the setActive method.

@frectonz commented on GitHub (Sep 4, 2025): @brunowego Setting the active organization is supported in the latest version of better auth. You can read more about it [here](https://www.better-auth.com/docs/plugins/organization#active-organization). Let me know if you are still facing a problem with the `setActive` method.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1522