npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

better-auth-organization-member

v1.0.8

Published

Better Auth plugin to list organization members with full user data

Readme

better-auth-organization-member

A Better Auth plugin that extends the organization plugin with additional member management capabilities.

Features

  • Update Member Endpoint: /organization/update-member - Update member information including role AND additional fields (firstName, lastName, avatar, and any custom fields)
  • Automatic Hook Injection: Automatically adds afterAcceptInvitation hook to the organization plugin to transfer invitation data from invitations to member records
  • Before/After Update Hooks: Lifecycle hooks for member updates
  • Full Type Inference: Automatically infers member additional fields from organization plugin schema

Requirements

  • better-auth >= 1.4.9
  • organization plugin must be enabled

Installation

npm install better-auth-organization-member
// or yarn
// yarn add better-auth-organization-member

Usage

Server Setup

Simply add the plugin after the organization plugin. No manual hooks required!

import { betterAuth } from 'better-auth';
import { organization } from 'better-auth/plugins';
import { organizationMember } from 'better-auth-organization-member';

export const auth = betterAuth({
  // ... other config
  plugins: [
    organization({
      // organization config
      schema: {
        member: {
          additionalFields: {
            firstName: { type: 'string', input: true, required: false },
            lastName: { type: 'string', input: true, required: false },
            avatar: { type: 'string', input: true, required: false },
          },
        },
        invitation: {
          additionalFields: {
            firstName: { type: 'string', input: true, required: false },
            lastName: { type: 'string', input: true, required: false },
            avatar: { type: 'string', input: true, required: false },
          },
        },
      },
    }),
    // This plugin automatically injects the afterAcceptInvitation hook
    organizationMember({
      schema: {
        member: {
          additionalFields: {
            firstName: { type: 'string', input: true, required: false },
            lastName: { type: 'string', input: true, required: false },
            avatar: { type: 'string', input: true, required: false },
          },
        }
      },
      organizationMemberHooks: {
        // Optional: Hook before member update
        async beforeUpdateMember(data) {
          console.log('Updating member:', data.member.id);
          // You can modify the updates here
          return {
            data: {
              ...data.updates,
              // Add custom logic
            },
          };
        },
        // Optional: Hook after member update
        async afterUpdateMember(data) {
          console.log('Member updated:', data.member.id);
        },
      },
    }),
  ],
});

What Happens Automatically

When the organizationMember plugin is added:

  1. ✅ It automatically injects an afterAcceptInvitation hook into the organization plugin
  2. ✅ When an invitation is accepted, the hook transfers firstName, lastName, and avatar from the invitation to the member record
  3. ✅ No need to manually add hooks in your provider configuration!

Client Usage

import { createAuthClient } from 'better-auth/client';
import { organizationClient } from 'better-auth/client/plugins';
import { organizationMemberClient, inferOrgMemberAdditionalFields } from 'better-auth-organization-member/client';
import type { auth } from './auth'; // import the auth object type only

const client = createAuthClient({
  plugins: [
    organizationClient({
      schema: inferOrgMemberAdditionalFields<typeof auth>()
    }),
    organizationMemberClient({
      schema: inferOrgMemberAdditionalFields<typeof auth>()
    })
  ],
});

Or if you can't import the auth object type, you can pass the schema directly:

import { createAuthClient } from 'better-auth/client';
import { organizationClient } from 'better-auth/client/plugins';
import { organizationMemberClient } from 'better-auth-organization-member/client';

const client = createAuthClient({
  plugins: [
    organizationClient({
      schema: {
        member: {
          additionalFields: {
            firstName: { type: 'string', input: true, required: false },
            lastName: { type: 'string', input: true, required: false },
            avatar: { type: 'string', input: true, required: false },
          },
        },
        invitation: {
          additionalFields: {
            firstName: { type: 'string', input: true, required: false },
            lastName: { type: 'string', input: true, required: false },
            avatar: { type: 'string', input: true, required: false },
          },
        },
      },
    }),
    organizationMemberClient({
      schema: {
        member: {
          additionalFields: {
            firstName: { type: 'string', input: true, required: false },
            lastName: { type: 'string', input: true, required: false },
            avatar: { type: 'string', input: true, required: false },
          },
        },
        invitation: {
          additionalFields: {
            firstName: { type: 'string', input: true, required: false },
            lastName: { type: 'string', input: true, required: false },
            avatar: { type: 'string', input: true, required: false },
          },
        },
      },
    }),
  ],
});

// Update member information (role + additional fields)
await client.organization.updateMember({
  memberId: 'member-id',
  data: {
    role: 'admin', // Can update role
    firstName: 'John',
    lastName: 'Doe',
    avatar: 'https://example.com/avatar.jpg',
    // any other custom fields defined in your member schema
  },
  fetchOptions: {
    headers: {
      'X-Custom-Header': 'value',
    },
  },
});

// Update invitation information
await client.organization.updateInvitation({
  invitationId: 'invitation-id',
  data: {
    role: 'admin', // Can update role
    firstName: 'John',
    lastName: 'Doe',
    avatar: 'https://example.com/avatar.jpg',
    // any other custom fields defined in your invitation schema
  },
  fetchOptions: {
    headers: {
      'X-Custom-Header': 'value',
    },
  },
});

// List invitations with filtering and sorting (overrides original listInvitations)
const result = await client.organization.listInvitations({
  query: {
    organizationId: 'org-id', // optional
    limit: 10,
    offset: 0,
    sortBy: 'createdAt',
    sortDirection: 'desc',
    filterField: 'status',
    filterValue: 'pending',
    filterOperator: 'eq',
  },
  fetchOptions: {
    headers: {
      'X-Custom-Header': 'value',
    },
  },
});
// Response: { invitations: [...], total: number }

API

Endpoint: /organization/update-member

Updates member information in an organization. Follows the exact same permission logic as /organization/update-member-role but accepts additional fields.

Method: POST

Body:

{
  memberId: string;                    // Required: ID of the member to update
  organizationId?: string;             // Optional: defaults to active organization
  data: {
    role?: string | string[];          // Optional: Role(s) to assign
    // ... any additional fields defined in member.additionalFields
    // Examples: firstName, lastName, avatar, etc.
  };
  fetchOptions?: BetterFetchOption;   // Optional: fetch options (headers, etc.)
}

Response:

{
  id: string;
  userId: string;
  organizationId: string;
  role: string;
  user: {
    id: string;
    email: string;
    name: string | null;
    image: string | null;
  };
  // ... all additional fields
}

Permissions: Same as updateMemberRole - requires member:update permission or owner/admin role.

Role Logic (same as updateMemberRole):

  • ✅ Owners can update any member
  • ✅ Admins with member:update permission can update members
  • ❌ Non-creators cannot update creators
  • ❌ Last owner cannot demote themselves
  • ❌ Same permission checks as the built-in role update

Endpoint: /organization/list-invitations

Lists invitations in an organization with filtering, sorting, and pagination support. This endpoint overrides the original listInvitations endpoint from the organization plugin.

Method: GET

Query Parameters:

{
  organizationId?: string;             // Optional: defaults to active organization
  organizationSlug?: string;           // Optional: organization slug instead of ID
  limit?: number;                      // Optional: number of invitations to return (default: 100)
  offset?: number;                     // Optional: offset to start from (default: 0)
  sortBy?: string;                     // Optional: field to sort by
  sortDirection?: "asc" | "desc";      // Optional: sort direction (default: "asc")
  filterField?: string;                // Optional: field to filter by
  filterValue?: string | number | boolean; // Optional: value to filter by
  filterOperator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "contains"; // Optional: filter operator
}

Response:

{
  invitations: Invitation[];  // Array of invitations
  total: number;              // Total count of invitations (before pagination)
}

Permissions: Requires membership in the organization.

Features:

  • Filtering: Filter by any field using operators (eq, ne, lt, lte, gt, gte, contains)
  • Sorting: Sort by any field in ascending or descending order
  • Pagination: Support for limit and offset
  • Organization support: Can filter by organizationId or organizationSlug
  • Same response format as listMembers: Returns { invitations, total } for consistency

Hooks

beforeUpdateMember

Called before a member's information is updated. Can modify the update data.

beforeUpdateMember?: (data: {
  member: Member;
  updates: Record<string, any>;
  user: User;
  organization: Organization;
}) => Promise<void | { data: Record<string, any> }>;

afterUpdateMember

Called after a member's information is updated.

afterUpdateMember?: (data: {
  member: Member;
  previousData: Record<string, any>;
  user: User;
  organization: Organization;
}) => Promise<void>;

Automatic afterAcceptInvitation Hook

This hook is automatically injected into the organization plugin when you add organizationMember(). It:

  1. Extracts all additional fields from invitation.additionalFields
  2. Automatically transfers them to the newly created member record
  3. Works with any custom fields you define in both invitation and member schemas
  4. Logs the update (or errors if they occur)
  5. Does not fail the invitation acceptance if the update fails

Comparison with updateMemberRole

Built-in updateMemberRole (organization plugin)

// Only updates the role field
await client.organization.updateMemberRole({
  memberId: 'member-id',
  role: 'admin',
});

This Plugin's updateMember

// Updates role AND additional fields in one call
await client.organization.updateMember({
  memberId: 'member-id',
  data: {
    role: 'admin',                                    // ✅ Can update role
    firstName: 'John',                                // ✅ Plus additional fields
    lastName: 'Doe',
    avatar: 'https://example.com/avatar.jpg',
    customField: 'value',                             // ✅ Plus any custom fields
  },
});

Key Features:

  • Exact same permission logic as updateMemberRole (identical role checks)
  • Same validation for role updates (creator protection, owner checks, etc.)
  • Additional field support - Update member additional fields in the same request
  • Full type inference - TypeScript autocomplete for all additional fields
  • Automatic schema validation - Uses toZodSchema to validate additional fields

Implementation Details

This plugin:

  1. Extends updateMemberRole: Uses the exact same permission logic, role validation, and error handling as updateMemberRole, but accepts additional fields via toZodSchema
  2. Auto-injects hooks: Uses the plugin's init() method to inject afterAcceptInvitation into the organization plugin's hooks
  3. Type-safe: Full TypeScript support with InferAdditionalFieldsFromPluginOptions for automatic type inference of all additional fields
  4. Uses getOrgAdapter: Leverages the organization plugin's adapter utilities for consistent behavior
  5. Dynamic field transfer: Automatically detects and transfers all fields defined in invitation.additionalFields to member records
  6. Production-ready: Follows Better Auth best practices and patterns

How It Works

  1. At initialization: The plugin reads the organization plugin's schema to get member additional fields
  2. Schema generation: Uses toZodSchema to generate validation schema from member.additionalFields
  3. Endpoint creation: Creates the /organization/update-member endpoint with merged schemas (base + additional fields)
  4. Hook injection: Injects afterAcceptInvitation to automatically transfer invitation fields to member
  5. Type inference: TypeScript automatically infers all additional fields for full autocomplete support

License

MIT License

Copyright (c) 2026 ShareRing

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.