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
afterAcceptInvitationhook 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.9organizationplugin must be enabled
Installation
npm install better-auth-organization-member
// or yarn
// yarn add better-auth-organization-memberUsage
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:
- ✅ It automatically injects an
afterAcceptInvitationhook into the organization plugin - ✅ When an invitation is accepted, the hook transfers
firstName,lastName, andavatarfrom the invitation to the member record - ✅ 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:updatepermission 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
limitandoffset - ✅ Organization support: Can filter by
organizationIdororganizationSlug - ✅ 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:
- Extracts all additional fields from
invitation.additionalFields - Automatically transfers them to the newly created member record
- Works with any custom fields you define in both invitation and member schemas
- Logs the update (or errors if they occur)
- 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
toZodSchemato validate additional fields
Implementation Details
This plugin:
- Extends
updateMemberRole: Uses the exact same permission logic, role validation, and error handling asupdateMemberRole, but accepts additional fields viatoZodSchema - Auto-injects hooks: Uses the plugin's
init()method to injectafterAcceptInvitationinto the organization plugin's hooks - Type-safe: Full TypeScript support with
InferAdditionalFieldsFromPluginOptionsfor automatic type inference of all additional fields - Uses
getOrgAdapter: Leverages the organization plugin's adapter utilities for consistent behavior - Dynamic field transfer: Automatically detects and transfers all fields defined in
invitation.additionalFieldsto member records - Production-ready: Follows Better Auth best practices and patterns
How It Works
- At initialization: The plugin reads the organization plugin's schema to get member additional fields
- Schema generation: Uses
toZodSchemato generate validation schema frommember.additionalFields - Endpoint creation: Creates the
/organization/update-memberendpoint with merged schemas (base + additional fields) - Hook injection: Injects
afterAcceptInvitationto automatically transfer invitation fields to member - 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.
