FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(organization): apply beforeAddTeamMember data and allow teamMember additionalFields by mrpmohiburrahman · Pull Request #10889 · better-auth/better-auth · GitHub

fix(organization): apply beforeAddTeamMember data and allow teamMember additionalFields - #10889

Open
mrpmohiburrahman wants to merge 3 commits into
better-auth:mainfrom
mrpmohiburrahman:fix/team-member-hook-data-and-additional-fields
Open

mrpmohiburrahman wants to merge 3 commits into
better-auth:mainfrom
mrpmohiburrahman:fix/team-member-hook-data-and-additional-fields

Conversation

mrpmohiburrahman commented Aug 19, 2026
edited by cubic-dev-ai Bot
Loading

Copy link
Copy Markdown

Fixes #10707.

What

beforeAddTeamMember is typed Promise<void | { data: Record<string, any> }>, the same as beforeCreateTeam and beforeUpdateTeam. It does not honour that return value. At routes/crud-team.ts the call site read the hook's result, tested it, and then did nothing with it:

if (response && typeof response === "object" && "data" in response) {
    // Allow the hook to modify the data
}

The plugin has ten "data" in response sites and this was the only one with an empty body. git log -L puts its origin in bba0a42 ("feat(organization): organization life cycle hooks"), the same PR that added the working siblings — so it shipped empty rather than having been broken later. The comment describes the opposite of what the block does.

Fixing that alone is not enough to make the reporter's case work. teamMember was also the only one of the plugin's six org-owned models whose schema option omitted additionalFields — the other five all carry it. Since transformInput iterates the declared fields (packages/core/src/db/adapter/factory.ts:210) and never Object.keys(data), an undeclared key is dropped silently before it reaches any adapter. So the hook had nothing it could legally write, and the two defects are only observable together.

That gap looks like scope drift rather than a decision: additionalFields landed in 93698af, whose title names exactly the four models that existed then; teamMember arrived later with only modelName + fields, and the next model added after it (organizationRole) did ship with additionalFields.

How

  • types.ts / organization.ts — give teamMember the additionalFields option and spread it into the model's fields, matching the five siblings.
  • routes/crud-team.ts — capture response.data and pass it to both insert paths. There are two: addTeamMemberWithLimit when teams.maximumMembersPerTeam is set, and findOrCreateTeamMember otherwise. Threading only the second would have left the seat-limited path silently dropping hook data, so both are covered and both are tested.
  • adapter.ts — thread the fields through to createTeamMemberWithKey, the single adapter.create({ model: "teamMember" }) in non-test source. They are spread first, so teamId / userId / membershipKey / createdAt are re-applied after and stay authoritative. That ordering is load-bearing, not tidiness:
    • the route validates the team, the target user's org membership and the user record before the hook runs, so a merged { data: { teamId: "…" } } would insert into a team that was never checked;
    • membershipKey is declared input: false, but nothing in packages/core/src/db/adapter/ enforces input: false — transformInput has no such check. The ordering is the only thing protecting the unique membership key.
  • adapter.ts — stripTeamMembershipKey now runs through filterOutputFields. Every teamMember-returning path used that hand-rolled helper instead of filterOutputFields, which is what enforces returned: false for the siblings; enabling additionalFields without this would have leaked any field declared returned: false.
  • client.ts — OrganizationClientOptions["schema"] listed five models and omitted teamMember too. Without this, declaring teamMember fields on the client fails to compile, and the documented inferOrgAdditionalFields pattern fails with TS2559: has no properties in common when teamMember is the only model with additional fields — which is exactly the reported use case.

computeTeamMembershipKey still hashes only [teamId, userId], so no additional field can perturb the dedup key or the unique index. Nothing under packages/core is touched.

Direction — worth a maintainer's call

I implemented the reading that the type signature promises. The honest counter-argument is that this hook's JSDoc never advertised the mutation: "You can return a data object to override the default data." appears seven times in types.ts and never for beforeAddTeamMember, and the docs example only validates and throws.

The alternative is narrowing the return type to Promise<void> and admitting the hook is validate-only. I did not take it because the TypeScript contract is unambiguous and identical to two hooks that do honour it, and because narrowing is a breaking type change for anyone returning data today — currently a silent no-op, afterwards a compile error — with no runtime benefit. If you would rather have the narrowing, say so and I will swap it; the failing test is already here either way.

Two smaller semantics I picked and would happily change:

  • Re-add applies nothing. Both adapter methods are find-or-create, so when the membership already exists the existing row is returned untouched. I think not silently rewriting an established membership is right, but it does differ from beforeUpdateTeam.
  • createdAt stays authoritative, unlike beforeCreateTeam, which lets the hook override it. One rule seemed easier to explain than two: the hook adds fields, it does not rewrite the row's identity or timestamp.

Scope

Section 3 of the issue — that only the dedicated endpoint fires teamMember hooks — is deliberately not here. The other three insert paths (crud-org.ts, crud-members.ts, crud-invites.ts) create rows without running this hook, so hook-supplied fields are absent there. Changing that changes when hooks fire, which is an API decision rather than a bug fix. It is now stated in the docs example so nobody adopts the pattern expecting full coverage, and I am glad to follow up if you want it.

Verification

Baseline captured on main @ 64da15b before any edit; pnpm test avoided per AGENTS.md, so this is the organization plugin suite.

Before After
vitest run src/plugins/organization/ 253 passed, 1 todo 257 passed, 1 todo
pnpm typecheck exit 2 exit 0
biome check (26 files) clean clean

Zero pre-existing failures, and no test that passed in the baseline fails now. All four new tests were watched failing before the fix:

  • should declare teamMember additional fields on the table — the schema spread, via getAuthTables
  • should apply data returned from beforeAddTeamMember — end to end on the find-or-create path, including returned: false filtering
  • applies the hook data on the seat-limited insert path — the maximumMembersPerTeam path the report never reaches
  • lets the client schema declare teamMember additional fields — the client-side gap

The compile-time half of the bug reproduces as TS2353 on the satisfies form and TS2769 on the organization({ … }) call form.

Changeset included (better-auth: patch).

Parts of this were written with AI assistance; I have reviewed the change and can discuss any of it.


Summary by cubic

Applies beforeAddTeamMember returned data and adds schema.teamMember.additionalFields, so member-level fields persist and round-trip like other org models. Previously the hook’s data was ignored and undeclared fields were dropped.

  • Threads additionalFields from the route through the adapter into both insert paths (find-or-create and seat-limited). teamId, userId, membershipKey, and createdAt remain authoritative and override hook data.
  • Filters teamMember responses via filterOutputFields to honor returned: false; membershipKey remains server-only.
  • Extends server types and client OrganizationClientOptions["schema"] to include teamMember.additionalFields, unblocking inferOrgAdditionalFields.
  • Docs clarify limits (hook runs only on addTeamMember, applies on insert only), require optional or defaultValue fields for other creation paths, and the example now declares the teamMember field it returns.

Migration

  • Declare schema.teamMember.additionalFields on both server and client if you want to store extra member-level data.
  • Make those fields optional or give them a defaultValue if you also add members via createOrganization, addMember, or acceptInvitation (these do not invoke the hook).
  • Do not rely on overriding teamId, userId, membershipKey, or createdAt in the hook; they are ignored.

Written for commit 50f9c94. Summary will update on new commits.

mrpmohiburrahman requested review from a team as code owners August 19, 2026 15:20
mrpmohiburrahman requested review from gustavovalverde and removed request for a team August 19, 2026 15:20

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

@mrpmohiburrahman is attempting to deploy a commit to the better-auth Team on Vercel.

A member of the Team first needs to authorize it.

better-release Bot added organization Org, teams, roles, permissions, admin, access control docs Documentation, demos labels Aug 19, 2026

greptile-apps Bot commented Aug 19, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds teamMember.additionalFields support and persists data returned by beforeAddTeamMember while preserving authoritative membership fields and filtering non-returnable output.

  • Threads hook-provided fields through both unrestricted and seat-limited team-member insertion paths.
  • Extends server and client schema typing for team-member additional fields.
  • Documents that other team-member creation paths do not invoke this hook and require optional fields or database defaults.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains within the accepted scope.

No blocking failure remains.

Reviews (3): Last reviewed commit: "docs(organization): declare the teamMemb..." | Re-trigger Greptile

cubic-dev-ai Bot left a comment
edited
Loading

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

1 issue found across 8 files

Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/better-auth/src/plugins/organization/organization.ts">

<violation number="1" location="packages/better-auth/src/plugins/organization/organization.ts:1037">
P1: When a `teamMember.additionalFields` entry is `required: true` without a database default, invitation acceptance and member additions that do not provide that field still insert without it, so the database rejects the row. Supply required values or defaults on every team-member insert path before registering this field as required.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

bytaesu commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

This PR addresses the same issue as #10824. I've cross-referenced for triage, and we'll continue with whichever direction is more appropriate.

This branch has not been deployed

No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation, demos organization Org, teams, roles, permissions, admin, access control

Projects

None yet

Development

Successfully merging this pull request may close these issues.

organization: beforeAddTeamMember silently discards its returned data, and teamMember is the only model without additionalFields

2 participants


Back | FazBrowse Home | New Git URL