Better Auth plugin to list organization members with full user data
npm install better-auth-organization-memberA Better Auth plugin that extends the organization plugin with additional member management capabilities.
- 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
- better-auth >= 1.4.9
- organization plugin must be enabled
``sh`
npm install better-auth-organization-member
// or yarn
// yarn add better-auth-organization-member
Simply add the plugin after the organization plugin. No manual hooks required!
`typescript
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);
},
},
}),
],
});
`
When the organizationMember plugin is added:
1. ✅ It automatically injects an afterAcceptInvitation hook into the organization pluginfirstName
2. ✅ When an invitation is accepted, the hook transfers , lastName, and avatar from the invitation to the member record
3. ✅ No need to manually add hooks in your provider configuration!
`typescript
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
}),
organizationMemberClient({
schema: inferOrgMemberAdditionalFields
})
],
});
`
Or if you can't import the auth object type, you can pass the schema directly:
`typescript
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 }
`
Updates member information in an organization. Follows the exact same permission logic as /organization/update-member-role but accepts additional fields.
Method: POST
Body:
`typescript`
{
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:
`typescript`
{
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):member:update
- ✅ Owners can update any member
- ✅ Admins with permission can update members
- ❌ Non-creators cannot update creators
- ❌ Last owner cannot demote themselves
- ❌ Same permission checks as the built-in role update
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:
`typescript`
{
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:
`typescript`
{
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)limit
- ✅ Sorting: Sort by any field in ascending or descending order
- ✅ Pagination: Support for and offsetorganizationId
- ✅ Organization support: Can filter by or organizationSluglistMembers
- ✅ Same response format as : Returns { invitations, total } for consistency
Called before a member's information is updated. Can modify the update data.
`typescript`
beforeUpdateMember?: (data: {
member: Member;
updates: Record
user: User;
organization: Organization;
}) => Promise
Called after a member's information is updated.
`typescript`
afterUpdateMember?: (data: {
member: Member;
previousData: Record
user: User;
organization: Organization;
}) => Promise
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
`typescript`
// Only updates the role field
await client.organization.updateMemberRole({
memberId: 'member-id',
role: 'admin',
});
`typescript`
// 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)toZodSchema
- ✅ 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 to validate additional fields
This plugin:
1. Extends updateMemberRole: Uses the exact same permission logic, role validation, and error handling as updateMemberRole, but accepts additional fields via toZodSchemainit()
2. Auto-injects hooks: Uses the plugin's method to inject afterAcceptInvitation into the organization plugin's hooksInferAdditionalFieldsFromPluginOptions
3. Type-safe: Full TypeScript support with for automatic type inference of all additional fieldsgetOrgAdapter
4. Uses : Leverages the organization plugin's adapter utilities for consistent behaviorinvitation.additionalFields
5. Dynamic field transfer: Automatically detects and transfers all fields defined in to member records
6. Production-ready: Follows Better Auth best practices and patterns
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/organization/update-member
3. Endpoint creation: Creates the endpoint with merged schemas (base + additional fields)afterAcceptInvitation` to automatically transfer invitation fields to member
4. Hook injection: Injects
5. Type inference: TypeScript automatically infers all additional fields for full autocomplete support
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.