[GH-ISSUE #8691] list-user-invitations crashes with "Cannot read properties of null (reading 'name')" when invitation references a deleted organization #11163

Closed
opened 2026-04-13 07:31:27 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @raihanbrillmark on GitHub (Mar 19, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8691

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

To Reproduce:

  1. Configure betterAuth with the organization plugin and MongoDB adapter
  2. Create an organization and send an invitation to a user email
  3. Delete the organization from the database (or let it become orphaned via any means)
  4. Call GET /api/auth/organization/list-user-invitations (e.g., via authClient.organization.listUserInvitations())
  5. Server throws 500: TypeError: Cannot read properties of null (reading 'name')

Current vs. Expected behavior

Current vs. Expected behavior:

Current: The endpoint crashes with a 500 error whenever any invitation in the database references an organization that no longer exists. The entire listUserInvitations call fails, even for invitations that have valid organizations.

Expected: Invitations with missing/deleted organizations should either be filtered out or returned with an empty organizationName — the endpoint should not throw.

The crash originates in dist/plugins/organization/adapter.mjs:

    return (await adapter.findMany({
        model: "invitation",
        where: [{ field: "email", value: email.toLowerCase() }],
        join: { organization: true }
    })).map(({ organization, ...inv }) => ({
        ...inv,
        organizationName: organization.name  // 💥 organization is null for orphaned invitations
    }));
} 

Suggested fix:


organizationName: organization?.name ?? ''
// or filter before map:
.filter(({ organization }) => organization != null)

What version of Better Auth are you using?

1.5.5

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 25.3.0: Wed Jan 28 20:55:08 PST 2026; root:xnu-12377.91.3~2/RELEASE_ARM64_T6020",
    "release": "25.3.0",
    "cpuCount": 10,
    "cpuModel": "Apple M2 Pro",
    "totalMemory": "16.00 GB",
    "freeMemory": "1.60 GB"
  },
  "node": {
    "version": "v24.4.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.6.2"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "^16.0.7"
    },
    {
      "name": "react",
      "version": "^19.2.1"
    }
  ],
  "databases": [
    {
      "name": "mongodb",
      "version": "^7.0.0"
    }
  ],
  "betterAuth": {
    "version": "Unknown",
    "config": null,
    "error": "Converting circular structure to JSON\n    --> starting at object with constructor 'Stripe'\n    |     property 'account' -> object with constructor 'Constructor'\n    --- property '_stripe' closes the circle"
  }
}

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

Client

Auth config (if applicable)

import { betterAuth } from "better-auth";
import { mongodbAdapter } from "better-auth/adapters/mongodb";
import { organization } from "better-auth/plugins";

export const auth = betterAuth({
  experimental: { joins: true },
  database: mongodbAdapter(db, { client }),
  plugins: [
    organization({
      // any config
    }),
  ],
});

Additional context

Reproducible locally. The issue is deterministic — it occurs whenever there is at least one invitation document in the database whose organizationId has no corresponding organization document. This can happen naturally when an organization is deleted without cascading deletes to its invitations.

Originally created by @raihanbrillmark on GitHub (Mar 19, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8691 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce **To Reproduce:** 1. Configure `betterAuth` with the `organization` plugin and MongoDB adapter 2. Create an organization and send an invitation to a user email 3. Delete the organization from the database (or let it become orphaned via any means) 4. Call `GET /api/auth/organization/list-user-invitations` (e.g., via `authClient.organization.listUserInvitations()`) 5. Server throws 500: `TypeError: Cannot read properties of null (reading 'name')` ### Current vs. Expected behavior **Current vs. Expected behavior:** **Current:** The endpoint crashes with a 500 error whenever *any* invitation in the database references an organization that no longer exists. The entire `listUserInvitations` call fails, even for invitations that have valid organizations. **Expected:** Invitations with missing/deleted organizations should either be filtered out or returned with an empty `organizationName` — the endpoint should not throw. The crash originates in `dist/plugins/organization/adapter.mjs`: ```ts listUserInvitations: async (email) => { return (await adapter.findMany({ model: "invitation", where: [{ field: "email", value: email.toLowerCase() }], join: { organization: true } })).map(({ organization, ...inv }) => ({ ...inv, organizationName: organization.name // 💥 organization is null for orphaned invitations })); } ``` Suggested fix: ```ts organizationName: organization?.name ?? '' // or filter before map: .filter(({ organization }) => organization != null) ``` ### What version of Better Auth are you using? 1.5.5 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 25.3.0: Wed Jan 28 20:55:08 PST 2026; root:xnu-12377.91.3~2/RELEASE_ARM64_T6020", "release": "25.3.0", "cpuCount": 10, "cpuModel": "Apple M2 Pro", "totalMemory": "16.00 GB", "freeMemory": "1.60 GB" }, "node": { "version": "v24.4.0", "env": "development" }, "packageManager": { "name": "npm", "version": "11.6.2" }, "frameworks": [ { "name": "next", "version": "^16.0.7" }, { "name": "react", "version": "^19.2.1" } ], "databases": [ { "name": "mongodb", "version": "^7.0.0" } ], "betterAuth": { "version": "Unknown", "config": null, "error": "Converting circular structure to JSON\n --> starting at object with constructor 'Stripe'\n | property 'account' -> object with constructor 'Constructor'\n --- property '_stripe' closes the circle" } } ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth"; import { mongodbAdapter } from "better-auth/adapters/mongodb"; import { organization } from "better-auth/plugins"; export const auth = betterAuth({ experimental: { joins: true }, database: mongodbAdapter(db, { client }), plugins: [ organization({ // any config }), ], }); ``` ### Additional context Reproducible locally. The issue is deterministic — it occurs whenever there is at least one `invitation` document in the database whose `organizationId` has no corresponding `organization` document. This can happen naturally when an organization is deleted without cascading deletes to its invitations.
GiteaMirror added the lockedbug labels 2026-04-13 07:31:27 -05:00
Author
Owner

@raihanbrillmark commented on GitHub (Mar 19, 2026):

Workaround (until this is fixed in the library)

Since this crashes inside better-auth's internals, I worked around it by creating a more specific Next.js App Router route that takes priority over the [...all] catch-all handler. Next.js resolves static routes before dynamic ones, so /api/auth/organization/list-user-invitations/route.ts intercepts the request before better-auth processes it.

The custom route queries MongoDB directly using an aggregation pipeline that skips orphaned invitations:

// src/app/api/auth/organization/list-user-invitations/route.ts
import { auth, db } from '@/auth/auth';
import { type NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
    try {
        const session = await auth.api.getSession({ headers: request.headers });
        if (!session?.user?.email) {
            return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
        }

        const email = session.user.email.toLowerCase();

        const invitations = await db
            .collection('invitation')
            .aggregate([
                { $match: { email } },
                {
                    $lookup: {
                        from: 'organization',
                        localField: 'organizationId',
                        foreignField: '_id',
                        as: 'organizationData',
                    },
                },
                // Skip invitations whose organization no longer exists
                { $match: { 'organizationData.0': { $exists: true } } },
                {
                    $addFields: {
                        organization: { $arrayElemAt: ['$organizationData', 0] },
                    },
                },
                { $project: { organizationData: 0 } },
            ])
            .toArray();

        const formatted = invitations.map(({ _id, organization, ...inv }) => ({
            id: String(_id),
            ...inv,
            organizationName: organization?.name ?? '',
        }));

        return NextResponse.json(formatted);
    } catch (error) {
        console.error('[list-user-invitations]', error);
        return NextResponse.json([], { status: 200 });
    }
}

Stack: Next.js App Router + MongoDB adapter + experimental: { joins: true }

The real fix in the library would be a one-liner in adapter.mjs:

// Change this:
organizationName: [organization.name](http://organization.name/)

// To this:
organizationName: organization?.name ?? ''
// or filter orphaned invitations before mapping:
.filter(({ organization }) => organization != null)

<!-- gh-comment-id:4090642628 --> @raihanbrillmark commented on GitHub (Mar 19, 2026): **Workaround (until this is fixed in the library)** Since this crashes inside better-auth's internals, I worked around it by creating a more specific Next.js App Router route that takes priority over the `[...all]` catch-all handler. Next.js resolves static routes before dynamic ones, so `/api/auth/organization/list-user-invitations/route.ts` intercepts the request before better-auth processes it. The custom route queries MongoDB directly using an aggregation pipeline that skips orphaned invitations: ```ts // src/app/api/auth/organization/list-user-invitations/route.ts import { auth, db } from '@/auth/auth'; import { type NextRequest, NextResponse } from 'next/server'; export async function GET(request: NextRequest) { try { const session = await auth.api.getSession({ headers: request.headers }); if (!session?.user?.email) { return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); } const email = session.user.email.toLowerCase(); const invitations = await db .collection('invitation') .aggregate([ { $match: { email } }, { $lookup: { from: 'organization', localField: 'organizationId', foreignField: '_id', as: 'organizationData', }, }, // Skip invitations whose organization no longer exists { $match: { 'organizationData.0': { $exists: true } } }, { $addFields: { organization: { $arrayElemAt: ['$organizationData', 0] }, }, }, { $project: { organizationData: 0 } }, ]) .toArray(); const formatted = invitations.map(({ _id, organization, ...inv }) => ({ id: String(_id), ...inv, organizationName: organization?.name ?? '', })); return NextResponse.json(formatted); } catch (error) { console.error('[list-user-invitations]', error); return NextResponse.json([], { status: 200 }); } } ``` **Stack:** Next.js App Router + MongoDB adapter + `experimental: { joins: true }` The real fix in the library would be a one-liner in `adapter.mjs`: ```ts // Change this: organizationName: [organization.name](http://organization.name/) // To this: organizationName: organization?.name ?? '' // or filter orphaned invitations before mapping: .filter(({ organization }) => organization != null) ```
Author
Owner

@dosubot[bot] commented on GitHub (Mar 19, 2026):

Hi @raihanbrillmark! I'm Dosu and I’m helping the better-auth team.

Thanks for the detailed bug report and workaround! I confirmed this is a valid bug in the codebase.

The crash originates in listUserInvitations (lines 920-937) where organization.name is accessed without a null check:

return invitations.map(({ organization, ...inv }) => ({
    ...inv,
    organizationName: organization.name,  // ← crashes when organization is null
}));

This was introduced in PR #6602 which added the organization name to invitation responses but didn't account for orphaned invitations. While deleteOrganization attempts to clean up invitations, edge cases can still leave orphaned records.

There's no existing fix for this yet. Your suggested one-liner fix (organization?.name ?? '' or filtering null organizations) would be the correct approach.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:4090676801 --> @dosubot[bot] commented on GitHub (Mar 19, 2026): <!-- Greeting --> Hi @raihanbrillmark! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> Thanks for the detailed bug report and workaround! I confirmed this is a valid bug in the codebase. The crash originates in [`listUserInvitations` (lines 920-937)](https://github.com/better-auth/better-auth/blob/a0211a05a6b21cbec8b2639cc063f0d04dc3a690/packages/better-auth/src/plugins/organization/adapter.ts#L920-L937) where `organization.name` is accessed without a null check: ```typescript return invitations.map(({ organization, ...inv }) => ({ ...inv, organizationName: organization.name, // ← crashes when organization is null })); ``` This was introduced in [PR #6602](https://github.com/better-auth/better-auth/pull/6602) which added the organization name to invitation responses but didn't account for orphaned invitations. While [`deleteOrganization`](https://github.com/better-auth/better-auth/blob/a0211a05a6b21cbec8b2639cc063f0d04dc3a690/packages/better-auth/src/plugins/organization/adapter.ts#L409-L438) attempts to clean up invitations, edge cases can still leave orphaned records. There's no existing fix for this yet. Your suggested one-liner fix (`organization?.name ?? ''` or filtering null organizations) would be the correct approach. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=brand-link).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/8d5fe723-1eff-49cf-b65b-b27fa6c0e8dc?feedback_type=great_response&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/8d5fe723-1eff-49cf-b65b-b27fa6c0e8dc?feedback_type=irrelevant_answer&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/8d5fe723-1eff-49cf-b65b-b27fa6c0e8dc?feedback_type=incorrect_sources&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/8d5fe723-1eff-49cf-b65b-b27fa6c0e8dc?feedback_type=too_verbose&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/8d5fe723-1eff-49cf-b65b-b27fa6c0e8dc?feedback_type=hallucination&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/8d5fe723-1eff-49cf-b65b-b27fa6c0e8dc?feedback_type=bug_report&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report) | [Other](https://app.dosu.dev/response-feedback/8d5fe723-1eff-49cf-b65b-b27fa6c0e8dc?feedback_type=other&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=chat-badge)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=Thanks%20for%20the%20detailed%20bug%20report%20and%20workaround%21%20I%20confirmed%20this%20is%20a%20valid%20bug%20in%20the%20codebase.%0A%0AThe%20crash%20originates%20in%20%5B%60listUserInvitations%60%20%28lines%20920-937%29%5D%28https%3A//github.com/better-auth/better-auth/blob/a0211a05a6b21cbec8b2639cc063f0d04dc3a690/packages/better-auth/src/plugins/organization/adapter.ts%23L920-L937%29%20where%20%60organization.name%60%20is%20accessed%20without%20a%20null%20check%3A%0A%0A%60%60%60typescript%0Areturn%20invitations.map%28%28%7B%20organization%2C%20...inv%20%7D%29%20%3D%3E%20%28%7B%0A%20%20%20%20...inv%2C%0A%20%20%20%20organizationName%3A%20organization.name%2C%20%20//%20%E2%86%90%20crashes%20when%20organization%20is%20null%0A%7D%29%29%3B%0A%60%60%60%0A%0AThis%20was%20introduced%20in%20%5BPR%20%236602%5D%28https%3A//github.com/better-auth/better-auth/pull/6602%29%20which%20added%20the%20organization%20name%20to%20invitation%20responses%20but%20didn%27t%20account%20for%20orphaned%20invitations.%20While%20%5B%60deleteOrganization%60%5D%28https%3A//github.com/better-auth/better-auth/blob/a0211a05a6b21cbec8b2639cc063f0d04dc3a690/packages/better-auth/src/plugins/organization/adapter.ts%23L409-L438%29%20attempts%20to%20clean%20up%20invitations%2C%20edge%20cases%20can%20still%20leave%20orphaned%20records.%0A%0AThere%27s%20no%20existing%20fix%20for%20this%20yet.%20Your%20suggested%20one-liner%20fix%20%28%60organization%3F.name%20%3F%3F%20%27%27%60%20or%20filtering%20null%20organizations%29%20would%20be%20the%20correct%20approach.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=join-discord)&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/8691)
Author
Owner

@raihanbrillmark commented on GitHub (Mar 19, 2026):

I have open a PR for this also: https://github.com/better-auth/better-auth/pull/8694

<!-- gh-comment-id:4091693780 --> @raihanbrillmark commented on GitHub (Mar 19, 2026): I have open a PR for this also: https://github.com/better-auth/better-auth/pull/8694
Author
Owner

@raihanbrillmark commented on GitHub (Mar 19, 2026):

PR has been merged: https://github.com/better-auth/better-auth/pull/8694

<!-- gh-comment-id:4092610366 --> @raihanbrillmark commented on GitHub (Mar 19, 2026): PR has been merged: https://github.com/better-auth/better-auth/pull/8694
Author
Owner

@github-actions[bot] commented on GitHub (Mar 31, 2026):

This issue has been locked as it was closed more than 7 days ago. If you're experiencing a similar problem or you have additional context, please open a new issue and reference this one.

<!-- gh-comment-id:4165912956 --> @github-actions[bot] commented on GitHub (Mar 31, 2026): This issue has been locked as it was closed more than 7 days ago. If you're experiencing a similar problem or you have additional context, please open a new issue and reference this one.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#11163