[GH-ISSUE #2050] Organization membership limit of 10000 causing 500 Internal Server Error #9026

Closed
opened 2026-04-13 04:17:58 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @KayaTheRock on GitHub (Mar 29, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/2050

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Title: Organization membership limit of 10000 causing 500 Internal Server Error

Description

We're experiencing an issue with Better Auth v1.2.5 where setting the organization membershipLimit above 100 causes the application to fail with a 500 Internal Server Error when trying to access organization details via the getFullOrganization endpoint.

Steps to Reproduce

  1. Configure Better Auth with an organization membership limit above 100:
organization({
  ac: ac,
  roles: {
    employer,
    moderator,
    candidateInvited,
    candidatePublic,
    owner
  },
  membershipLimit: 101  // Any value above 100 fails
})
  1. Create an organization with 101 members
  2. Attempt to access organization details, better-auth tries the getFullOrganization endpoint and sends back 500

Current vs. Expected behavior

Expected Behavior

The application should successfully retrieve organization details as long as the actual member count is below the configured limit.

Actual Behavior

When setting the membership limit to any value above 100 (even 101), the application returns a 500 Internal Server Error when trying to access the organization details. The same code works perfectly when the limit is set to 100 or lower.

What version of Better Auth are you using?

1.2.5

Provide environment information

## Environment Details
- Better Auth version: 1.2.5
- Node.js version: 22.14.0
- Database: PostgreSQL
- Framework: Next.js

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth";
import { Pool } from "pg";
import { nextCookies } from "better-auth/next-js";
import { admin, organization } from "better-auth/plugins"
import { ac, employer, candidateInvited, candidatePublic, owner, moderator } from "./permissions"
import { NextRequest } from 'next/server';
import { cookies } from 'next/headers';


interface GitHubProfile {
  id: number;
  email: string;
  name?: string;
  login: string;
  avatar_url?: string;
}

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

// Filter out undefined values from trusted origins
const trustedOrigins = [
  process.env.NEXT_PUBLIC_APP_URL,
].filter((origin): origin is string => !!origin);

// Determine if we're in a secure environment
const isSecureEnv = process.env.NODE_ENV === 'production' || 
                   process.env.NEXT_PUBLIC_APP_URL?.startsWith('https://') ||
                   process.env.BETTER_AUTH_URL?.startsWith('https://');

// Get the domain from the app URL
function getDomain() {
  const appUrl = process.env.NEXT_PUBLIC_APP_URL;
  if (!appUrl) return undefined;

  try {
    const url = new URL(appUrl);
    // For development or ngrok, don't set domain
    if (process.env.NODE_ENV !== 'production' || url.hostname.includes('ngrok-free.app')) {
      return undefined;
    }
    return url.hostname;
  } catch {
    return undefined;
  }
}

// Cookie configuration based on environment
const cookieConfig = {
  name: 'better-auth.session_token',
  httpOnly: true,
  path: '/',
  sameSite: isSecureEnv ? 'none' as const : 'lax' as const,
  secure: isSecureEnv,
  // Don't use prefix for ngrok as it can cause issues
  prefix: isSecureEnv && !process.env.NEXT_PUBLIC_APP_URL?.includes('ngrok-free.app') ? '__Secure-' : undefined,
  domain: getDomain()
};

export const auth = betterAuth({
  database: pool,
  trustedOrigins,
  session: {
    maxAge: 30 * 24 * 60 * 60, // 30 days in seconds
    freshAge: 24 * 60 * 60, // 24 hours in seconds
    modelName: "session",
    createTable: true,
    cookie: cookieConfig
  },
  emailAndPassword: {
    enabled: true,
    autoSignIn: false
  },
  socialProviders: {
    github: {
      clientId: process.env.GITHUB_CLIENT_ID as string,
      clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
      scope: ['user:email'],
      enabled: Boolean(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET)
    }
  },
  plugins: [
    nextCookies(),
    admin(),
    organization({
      ac: ac,
      roles: {
        employer,
        moderator,
        candidateInvited,
        candidatePublic,
        owner
      },
      membershipLimit: 101
    }),
  ],
})

// Export type-safe auth client
export type Auth = typeof auth;

export function getSessionCookie(req: NextRequest): string | null {
  // First try to get the better-auth cookie (preferred)
  const betterAuthCookie = req.cookies.get('better-auth.session_token')?.value;
  if (betterAuthCookie) {
    return betterAuthCookie;
  }
  
  // Try to get from request cookies with both secure and non-secure prefixes
  const secureCookieValue = req.cookies.get('__Secure-session')?.value;
  const regularCookieValue = req.cookies.get('session')?.value;
  
  if (secureCookieValue || regularCookieValue) {
    return secureCookieValue || regularCookieValue || null;
  }

  // If not found in request, try to get from the server cookies
  const serverCookies = cookies();
  const serverBetterAuthCookie = serverCookies.get('better-auth.session_token')?.value;
  if (serverBetterAuthCookie) {
    return serverBetterAuthCookie;
  }
  
  const serverSecureCookieValue = serverCookies.get('__Secure-session')?.value;
  const serverRegularCookieValue = serverCookies.get('session')?.value;
  
  return serverSecureCookieValue || serverRegularCookieValue || null;
}

Additional context

Additional Information

We've determined through testing that the issue appears to be with the limit value itself, not with the actual number of members in the organization. When setting the limit to 100, everything works fine. When increasing it to 101 or higher, it fails with a 500 error.
We've tried:

  1. Resetting and rebuilding the application
  2. Verifying there are no orphaned member records
  3. Checking database integrity
  4. Lowering the limit to find a working threshold

It seems Better Auth may have a hard-coded internal limitation of 100 members that conflicts with the ability to set higher values in the configuration.

Originally created by @KayaTheRock on GitHub (Mar 29, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/2050 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Title: Organization membership limit of 10000 causing 500 Internal Server Error ## Description We're experiencing an issue with Better Auth v1.2.5 where setting the organization `membershipLimit` above 100 causes the application to fail with a 500 Internal Server Error when trying to access organization details via the `getFullOrganization` endpoint. ## Steps to Reproduce 1. Configure Better Auth with an organization membership limit above 100: ```javascript organization({ ac: ac, roles: { employer, moderator, candidateInvited, candidatePublic, owner }, membershipLimit: 101 // Any value above 100 fails }) ``` 2. Create an organization with 101 members 3. Attempt to access organization details, better-auth tries the `getFullOrganization` endpoint and sends back 500 ### Current vs. Expected behavior ## Expected Behavior The application should successfully retrieve organization details as long as the actual member count is below the configured limit. ## Actual Behavior When setting the membership limit to any value above 100 (even 101), the application returns a 500 Internal Server Error when trying to access the organization details. The same code works perfectly when the limit is set to 100 or lower. ### What version of Better Auth are you using? 1.2.5 ### Provide environment information ```bash ## Environment Details - Better Auth version: 1.2.5 - Node.js version: 22.14.0 - Database: PostgreSQL - Framework: Next.js ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth"; import { Pool } from "pg"; import { nextCookies } from "better-auth/next-js"; import { admin, organization } from "better-auth/plugins" import { ac, employer, candidateInvited, candidatePublic, owner, moderator } from "./permissions" import { NextRequest } from 'next/server'; import { cookies } from 'next/headers'; interface GitHubProfile { id: number; email: string; name?: string; login: string; avatar_url?: string; } const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); // Filter out undefined values from trusted origins const trustedOrigins = [ process.env.NEXT_PUBLIC_APP_URL, ].filter((origin): origin is string => !!origin); // Determine if we're in a secure environment const isSecureEnv = process.env.NODE_ENV === 'production' || process.env.NEXT_PUBLIC_APP_URL?.startsWith('https://') || process.env.BETTER_AUTH_URL?.startsWith('https://'); // Get the domain from the app URL function getDomain() { const appUrl = process.env.NEXT_PUBLIC_APP_URL; if (!appUrl) return undefined; try { const url = new URL(appUrl); // For development or ngrok, don't set domain if (process.env.NODE_ENV !== 'production' || url.hostname.includes('ngrok-free.app')) { return undefined; } return url.hostname; } catch { return undefined; } } // Cookie configuration based on environment const cookieConfig = { name: 'better-auth.session_token', httpOnly: true, path: '/', sameSite: isSecureEnv ? 'none' as const : 'lax' as const, secure: isSecureEnv, // Don't use prefix for ngrok as it can cause issues prefix: isSecureEnv && !process.env.NEXT_PUBLIC_APP_URL?.includes('ngrok-free.app') ? '__Secure-' : undefined, domain: getDomain() }; export const auth = betterAuth({ database: pool, trustedOrigins, session: { maxAge: 30 * 24 * 60 * 60, // 30 days in seconds freshAge: 24 * 60 * 60, // 24 hours in seconds modelName: "session", createTable: true, cookie: cookieConfig }, emailAndPassword: { enabled: true, autoSignIn: false }, socialProviders: { github: { clientId: process.env.GITHUB_CLIENT_ID as string, clientSecret: process.env.GITHUB_CLIENT_SECRET as string, scope: ['user:email'], enabled: Boolean(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) } }, plugins: [ nextCookies(), admin(), organization({ ac: ac, roles: { employer, moderator, candidateInvited, candidatePublic, owner }, membershipLimit: 101 }), ], }) // Export type-safe auth client export type Auth = typeof auth; export function getSessionCookie(req: NextRequest): string | null { // First try to get the better-auth cookie (preferred) const betterAuthCookie = req.cookies.get('better-auth.session_token')?.value; if (betterAuthCookie) { return betterAuthCookie; } // Try to get from request cookies with both secure and non-secure prefixes const secureCookieValue = req.cookies.get('__Secure-session')?.value; const regularCookieValue = req.cookies.get('session')?.value; if (secureCookieValue || regularCookieValue) { return secureCookieValue || regularCookieValue || null; } // If not found in request, try to get from the server cookies const serverCookies = cookies(); const serverBetterAuthCookie = serverCookies.get('better-auth.session_token')?.value; if (serverBetterAuthCookie) { return serverBetterAuthCookie; } const serverSecureCookieValue = serverCookies.get('__Secure-session')?.value; const serverRegularCookieValue = serverCookies.get('session')?.value; return serverSecureCookieValue || serverRegularCookieValue || null; } ``` ### Additional context ## Additional Information We've determined through testing that the issue appears to be with the limit value itself, not with the actual number of members in the organization. When setting the limit to 100, everything works fine. When increasing it to 101 or higher, it fails with a 500 error. We've tried: 1. Resetting and rebuilding the application 2. Verifying there are no orphaned member records 3. Checking database integrity 4. Lowering the limit to find a working threshold It seems Better Auth may have a hard-coded internal limitation of 100 members that conflicts with the ability to set higher values in the configuration.
GiteaMirror added the lockedbug labels 2026-04-13 04:17:58 -05:00
Author
Owner

@KayaTheRock commented on GitHub (Mar 29, 2025):

after some debugging prompts:

After a thorough investigation, I've identified the root cause of your issue. The problem is with how the context is being passed between components in the Better Auth library.
Here's what's happening:
When you configure the organization plugin with membershipLimit: 200, this value is correctly stored in the plugin options.
When the getFullOrganization endpoint is called, it passes ctx.context.orgOptions to the getOrgAdapter function.
In the adapter, the code uses options?.membershipLimit || 100 to determine the limit, which should work correctly if options is properly passed.
The critical bug is in the middleware setup. In the orgMiddleware function (in call.ts), it sets up an empty object with type assertion:
Apply to custom-plugi...
This is where the problem is! It's creating an empty object and just asserting it has the type with the options, but it's not actually passing the real configured options through. This is a classic TypeScript issue where type assertions hide runtime problems.
When getFullOrganization tries to use ctx.context.orgOptions?.membershipLimit, it's undefined because the actual options aren't properly passed through the middleware chain to the context.
Since options?.membershipLimit || 100 evaluates to the default 100 (because options.membershipLimit is undefined), it always fetches a maximum of 100 members regardless of what you configured.
To fix this issue:
The organization plugin needs to properly pass the configured options to the middleware
The middleware needs to actually include those options in the context, not just assert the type
This is why you're hitting the limit at 101 members - the code is always using 100 as the limit regardless of your configuration.

<!-- gh-comment-id:2763353107 --> @KayaTheRock commented on GitHub (Mar 29, 2025): after some debugging prompts: After a thorough investigation, I've identified the root cause of your issue. The problem is with how the context is being passed between components in the Better Auth library. Here's what's happening: When you configure the organization plugin with membershipLimit: 200, this value is correctly stored in the plugin options. When the getFullOrganization endpoint is called, it passes ctx.context.orgOptions to the getOrgAdapter function. In the adapter, the code uses options?.membershipLimit || 100 to determine the limit, which should work correctly if options is properly passed. The critical bug is in the middleware setup. In the orgMiddleware function (in call.ts), it sets up an empty object with type assertion: Apply to custom-plugi... This is where the problem is! It's creating an empty object and just asserting it has the type with the options, but it's not actually passing the real configured options through. This is a classic TypeScript issue where type assertions hide runtime problems. When getFullOrganization tries to use ctx.context.orgOptions?.membershipLimit, it's undefined because the actual options aren't properly passed through the middleware chain to the context. Since options?.membershipLimit || 100 evaluates to the default 100 (because options.membershipLimit is undefined), it always fetches a maximum of 100 members regardless of what you configured. To fix this issue: The organization plugin needs to properly pass the configured options to the middleware The middleware needs to actually include those options in the context, not just assert the type This is why you're hitting the limit at 101 members - the code is always using 100 as the limit regardless of your configuration.
Author
Owner

@KayaTheRock commented on GitHub (Mar 29, 2025):

I made a workaround using custom plugins


import { organization as originalOrganization } from "better-auth/plugins";

/**
 * A minimal workaround for the 500 error with organizations having > 100 members
 */
export function patchedOrganization(options = {}) {
  // Always ensure a high membership limit is set
  const patchedOptions = {
    ...options,
    membershipLimit: options.membershipLimit || 10000
  };
  
  // Get the original plugin with our patched options
  const plugin = originalOrganization(patchedOptions);
  
  // Create a new plugin object to avoid modifying the original one
  const patchedPlugin = { ...plugin };
  
  // We need to be more careful with accessing the init method
  if (plugin && typeof plugin.init === 'function') {
    // Store the original init function
    const originalInit = plugin.init;
    
    // Override the init function
    patchedPlugin.init = function(context) {
      try {
        // Call the original init
        const result = originalInit.call(this, context);
        
        // Apply our patches to the context
        patchContext(context);
        
        return result;
      } catch (error) {
        console.warn('Error in original init, applying patches directly:', error);
        // If original init fails, apply our patches anyway
        patchContext(context);
        return context;
      }
    };
  } else {
    // If no init method exists, create a simple one
    patchedPlugin.init = function(context) {
      // Apply our patches to the context
      patchContext(context);
      return context;
    };
  }
  
  return patchedPlugin;
}

/**
 * Helper function to patch the context with our fixes
 */
function patchContext(context) {
  if (!context) return;
  
  // Force our high limit into the context options
  if (!context.orgOptions) {
    context.orgOptions = { membershipLimit: 10000 };
  } else {
    context.orgOptions.membershipLimit = 10000;
  }
  
  // Patch the adapter's findMany method if it exists
  if (context.adapter && typeof context.adapter.findMany === 'function') {
    const originalFindMany = context.adapter.findMany;
    context.adapter.findMany = async function(params) {
      // For member and user queries, ensure a high limit
      if (params && (params.model === 'member' || params.model === 'user')) {
        return originalFindMany.call(this, {
          ...params,
          limit: params.limit > 10000 ? params.limit : 10000
        });
      }
      return originalFindMany.call(this, params);
    };
  }
  
  // Patch API methods if they exist
  if (context.api) {
    if (context.api.getFullOrganization && typeof context.api.getFullOrganization === 'function') {
      const originalGetFullOrg = context.api.getFullOrganization;
      context.api.getFullOrganization = async function(params) {
        // Add a high limit to query params
        const enhancedParams = {
          ...params,
          query: {
            ...params?.query,
            limit: 10000
          }
        };
        return originalGetFullOrg.call(this, enhancedParams);
      };
    }
  }
} 

import { organizationClient as originalOrganizationClient } from "better-auth/client/plugins";

/**
 * A minimal client-side workaround for the 500 error with organizations having > 100 members
 */
export function patchedOrganizationClient(options = {}) {
  // Always ensure a high membership limit is set
  const patchedOptions = {
    ...options,
    membershipLimit: options.membershipLimit || 10000
  };
  
  // Get the original client plugin with our patched options
  const originalPlugin = originalOrganizationClient(patchedOptions);
  
  // Create a new plugin object to avoid modifying the original
  const patchedPlugin = { ...originalPlugin };
  
  // Enhance the getFullOrganization method to ensure high limits
  if (patchedPlugin.getFullOrganization && typeof patchedPlugin.getFullOrganization === 'function') {
    const originalGetFullOrg = patchedPlugin.getFullOrganization;
    
    patchedPlugin.getFullOrganization = async function(params) {
      // Add or update the limit parameter in the query
      const enhancedParams = {
        ...params,
        query: {
          ...params?.query,
          limit: 10000
        }
      };
      
      try {
        return await originalGetFullOrg.call(this, enhancedParams);
      } catch (error) {
        console.error('Error in getFullOrganization:', error);
        throw error;
      }
    };
  }  
  return patchedPlugin;
} 
<!-- gh-comment-id:2763361016 --> @KayaTheRock commented on GitHub (Mar 29, 2025): I made a workaround using custom plugins ------------------------------------------------------------ ``` import { organization as originalOrganization } from "better-auth/plugins"; /** * A minimal workaround for the 500 error with organizations having > 100 members */ export function patchedOrganization(options = {}) { // Always ensure a high membership limit is set const patchedOptions = { ...options, membershipLimit: options.membershipLimit || 10000 }; // Get the original plugin with our patched options const plugin = originalOrganization(patchedOptions); // Create a new plugin object to avoid modifying the original one const patchedPlugin = { ...plugin }; // We need to be more careful with accessing the init method if (plugin && typeof plugin.init === 'function') { // Store the original init function const originalInit = plugin.init; // Override the init function patchedPlugin.init = function(context) { try { // Call the original init const result = originalInit.call(this, context); // Apply our patches to the context patchContext(context); return result; } catch (error) { console.warn('Error in original init, applying patches directly:', error); // If original init fails, apply our patches anyway patchContext(context); return context; } }; } else { // If no init method exists, create a simple one patchedPlugin.init = function(context) { // Apply our patches to the context patchContext(context); return context; }; } return patchedPlugin; } /** * Helper function to patch the context with our fixes */ function patchContext(context) { if (!context) return; // Force our high limit into the context options if (!context.orgOptions) { context.orgOptions = { membershipLimit: 10000 }; } else { context.orgOptions.membershipLimit = 10000; } // Patch the adapter's findMany method if it exists if (context.adapter && typeof context.adapter.findMany === 'function') { const originalFindMany = context.adapter.findMany; context.adapter.findMany = async function(params) { // For member and user queries, ensure a high limit if (params && (params.model === 'member' || params.model === 'user')) { return originalFindMany.call(this, { ...params, limit: params.limit > 10000 ? params.limit : 10000 }); } return originalFindMany.call(this, params); }; } // Patch API methods if they exist if (context.api) { if (context.api.getFullOrganization && typeof context.api.getFullOrganization === 'function') { const originalGetFullOrg = context.api.getFullOrganization; context.api.getFullOrganization = async function(params) { // Add a high limit to query params const enhancedParams = { ...params, query: { ...params?.query, limit: 10000 } }; return originalGetFullOrg.call(this, enhancedParams); }; } } } ``` ----------------------------------------------- ``` import { organizationClient as originalOrganizationClient } from "better-auth/client/plugins"; /** * A minimal client-side workaround for the 500 error with organizations having > 100 members */ export function patchedOrganizationClient(options = {}) { // Always ensure a high membership limit is set const patchedOptions = { ...options, membershipLimit: options.membershipLimit || 10000 }; // Get the original client plugin with our patched options const originalPlugin = originalOrganizationClient(patchedOptions); // Create a new plugin object to avoid modifying the original const patchedPlugin = { ...originalPlugin }; // Enhance the getFullOrganization method to ensure high limits if (patchedPlugin.getFullOrganization && typeof patchedPlugin.getFullOrganization === 'function') { const originalGetFullOrg = patchedPlugin.getFullOrganization; patchedPlugin.getFullOrganization = async function(params) { // Add or update the limit parameter in the query const enhancedParams = { ...params, query: { ...params?.query, limit: 10000 } }; try { return await originalGetFullOrg.call(this, enhancedParams); } catch (error) { console.error('Error in getFullOrganization:', error); throw error; } }; } return patchedPlugin; } ```
Author
Owner

@ping-maxwell commented on GitHub (Jun 20, 2025):

Hey @KayaTheRock can you confirm you're still having this issue on latest?
I looked through our codebase and I can understand what you're saying, however we have custom implementation which adds those missing context options in:

Image
<!-- gh-comment-id:2991066180 --> @ping-maxwell commented on GitHub (Jun 20, 2025): Hey @KayaTheRock can you confirm you're still having this issue on latest? I looked through our codebase and I can understand what you're saying, however we have custom implementation which adds those missing context options in: <img width="1042" alt="Image" src="https://github.com/user-attachments/assets/a8198be1-1155-4af5-b6e8-2ff77132be82" />
Author
Owner

@KayaTheRock commented on GitHub (Jul 17, 2025):

Just had the time to remove my custom plugin, updated to latest, and test. It works, thanks.

<!-- gh-comment-id:3084457153 --> @KayaTheRock commented on GitHub (Jul 17, 2025): Just had the time to remove my custom plugin, updated to latest, and test. It works, thanks.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9026