Skip to content

feat!: add batch endpoint for adding organization members#24456

Closed
jakehwll wants to merge 1 commit into
mainfrom
jakehwll/devex-112-organizations-batch-endpoint
Closed

feat!: add batch endpoint for adding organization members#24456
jakehwll wants to merge 1 commit into
mainfrom
jakehwll/devex-112-organizations-batch-endpoint

Conversation

@jakehwll

@jakehwll jakehwll commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

🤖 This PR was written by Coder Agent on behalf of Jake Howell

Add POST /organizations/{organization}/members that accepts an array of user IDs and adds them all in a single transaction, replacing the need for N individual POST calls when adding multiple members from the UI.

The existing single-member POST /organizations/{organization}/members/{user} endpoint is marked as deprecated but remains functional.

Backend:

  • AddOrganizationMembersRequest SDK type with user_ids field
  • postOrganizationMembers handler — validates all users up-front, inserts in a single InTx (atomic)
  • PostOrganizationMembers SDK client method

Frontend:

  • API.addOrganizationMembers API method
  • addOrganizationMembers query mutation (replaces the single-member variant)
  • OrganizationMembersPage wired to use the batch mutation

Depends on #24429

@jakehwll jakehwll force-pushed the jakehwll/devex-112-organizations-batch-endpoint branch from 4bff9cb to 163988a Compare April 17, 2026 04:13
@jakehwll jakehwll changed the base branch from main to jakehwll/DEVEX-112-organization-add-users-modal April 17, 2026 04:13
@jakehwll jakehwll force-pushed the jakehwll/devex-112-organizations-batch-endpoint branch from 163988a to 2fc9987 Compare April 17, 2026 04:21
@jakehwll jakehwll changed the title feat(coderd): add batch endpoint for adding organization members feat: add batch endpoint for adding organization members Apr 17, 2026
@jakehwll jakehwll marked this pull request as ready for review April 17, 2026 04:29
@jakehwll jakehwll requested a review from jeremyruppel April 17, 2026 04:29
@jakehwll jakehwll changed the title feat: add batch endpoint for adding organization members feat!: add batch endpoint for adding organization members Apr 17, 2026
@jakehwll jakehwll added the release/breaking This label is applied to PRs to detect breaking changes as part of the release process label Apr 17, 2026
@coder-tasks

coder-tasks Bot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Documentation Check

Updates Needed

  • docs/reference/api/members.md — The deprecated POST /organizations/{organization}/members/{user} endpoint still has no visible deprecation notice in the rendered markdown. The // @Deprecated tag was added in coderd/members.go, but the // @Summary annotation remains Add organization member with no "(deprecated)" suffix. As a result, the generated docs/reference/api/members.md heading is still ## Add organization member with no visual indicator. Fix: update the // @Summary annotation (line 25 of coderd/members.go) to Add organization member (deprecated) and regenerate the docs — the pattern ## Get workspace quota by user deprecated is already used elsewhere in the codebase.

Automated review via Coder Tasks

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2fc9987bfd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coderd/members.go Outdated
Comment thread coderd/members.go Outdated
@jakehwll jakehwll marked this pull request as draft April 17, 2026 04:39
@jakehwll jakehwll marked this pull request as ready for review April 17, 2026 04:46
@jakehwll jakehwll requested a review from pawbana April 17, 2026 04:46

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9693c23acb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coderd/members.go Outdated
Comment thread coderd/members.go Outdated
Comment thread site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx
Comment thread coderd/members.go Outdated
return
}

httpapi.Write(ctx, rw, http.StatusOK, resp)

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.

nit: this feels like a 201 to me

Suggested change
httpapi.Write(ctx, rw, http.StatusOK, resp)
httpapi.Write(ctx, rw, http.StatusCreated, resp)

Comment thread coderd/members.go Outdated
// Use the request context (not AsSystemRestricted) so that dbauthz
// enforces the caller has read permission on each target user,
// matching the authz behavior of the single-member endpoint which
// runs through ExtractUserParam.

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.

I'm not that familiar with permissions system but this part smells to me a bit.
Why permission check is a custom for loop in this endpoint implementation? Shouldn't it be some common function.

Comment thread coderd/members.go Outdated
user, err := api.Database.GetUserByID(ctx, uid)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("User %q not found.", uid),

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.

ExtractUserParam uses different logic for returning codes:

        user, err := db.GetUserByID(ctx, apiKey.UserID)
		if httpapi.Is404Error(err) {
			httpapi.ResourceNotFound(rw)
			return database.User{}, false
		}
		if err != nil {
			httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
				Message: "Internal error fetching user.",
				Detail:  err.Error(),
			})
			return database.User{}, false
		}

Comment thread coderd/members.go Outdated
Roles: []string{},
})
if database.IsUniqueViolation(err, database.UniqueOrganizationMembersPkey) {
return xerrors.Errorf("user %q is already a member of %q", user.Username, organization.DisplayName)

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.

endpoint gets user ids as input, user name in response would not be helpful to understand which ID is causing issues.

This is more of a nit but I'm also not sure if this case should return error. If user is already there it could just be skipped. No error would make it easier to add whole group of users without needing to filter / know if some users are already in org.
I think it makes sense in single user case but not necessarily in batch request.

Comment thread coderd/members.go Outdated
if !api.manualOrganizationMembership(ctx, rw, user) {
return
}
users = append(users, user)

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.

This slice seems redundant if usernames won't be returned in error message.

Comment thread coderd/members.go Outdated
return nil
}, nil)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{

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.

I think different status should be returned depending on error, probably mostly internal server error instead of StatusBadRequest?

Comment thread coderd/members.go
auditUsers = users

resp, err := convertOrganizationMembers(ctx, api.Database, allMembers)
if err != nil {

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.

single user endpoint also checks:

	if len(resp) == 0 {
		httpapi.InternalServerError(rw, xerrors.Errorf("marshal member"))
		return
	}

Comment thread codersdk/users.go
// PostOrganizationMember adds a user to an organization
// PostOrganizationMember adds a user to an organization.
//
// Deprecated: Use PostOrganizationMembers instead.

@pawbana pawbana Apr 17, 2026

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.

Does it need to be deprecated? It has a bit different input, single-member endpoint accepts ID, username, or "me".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I would prefer to deprecate it, else we have two seperate ways to input organization members to the database forevermore 🙂

Comment thread coderd/members.go Outdated
// runs through ExtractUserParam.
users := make([]database.User, 0, len(req.UserIDs))
for _, uid := range req.UserIDs {
user, err := api.Database.GetUserByID(ctx, uid)

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.

Would it be possible to single batch user lookup?

Base automatically changed from jakehwll/DEVEX-112-organization-add-users-modal to main April 21, 2026 03:43
jakehwll

This comment was marked as resolved.

@jakehwll jakehwll requested a review from pawbana April 23, 2026 12:24
@jakehwll jakehwll removed the request for review from jdomeracki-coder April 23, 2026 12:29
@jakehwll jakehwll force-pushed the jakehwll/devex-112-organizations-batch-endpoint branch from cbdbb4e to ac7842c Compare April 23, 2026 12:32
Comment thread coderd/members.go Outdated
// organization so we never hit a unique-violation inside the
// transaction. This lets callers add a group of users without
// caring which ones are already present.
existingRows, err := api.Database.OrganizationMembers(ctx, database.OrganizationMembersParams{

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.

How about INSERT ... ON CONFLICT DO NOTHING

This would also help with race condition where user is added to organization between existingRows are read and transaction starts.

Comment thread coderd/members.go
// Resolve all users in a single query. The request context
// (not AsSystemRestricted) is used so dbauthz enforces read
// permission on each target user.
users, err := api.Database.GetUsersByIDs(ctx, req.UserIDs)

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.

nit: AI says that GetUsersByIDs also returns deleted users.

Apparently this is already present in postOrganizationMember endpoint but I think it would make sense to return error in such case (adding deleted user to org)?
Something like:

var deleted []uuid.UUID
  for _, user := range users {
  	if user.Deleted {
  		deleted = append(deleted, user.ID)
  	}
  }

  if len(deleted) > 0 {
  	httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
  		Message: fmt.Sprintf("Deleted users cannot be added to an organization: %v", deleted),
  	})
  	return
  }

@jakehwll jakehwll force-pushed the jakehwll/devex-112-organizations-batch-endpoint branch from ac7842c to 6bc2766 Compare May 1, 2026 01:06
Add POST /organizations/{organization}/members that accepts an array of
user IDs and adds them all in a single transaction, replacing the need
for N individual POST calls when adding multiple members from the UI.

The existing single-member POST /organizations/{organization}/members/{user}
endpoint is marked as deprecated but remains functional.

Backend:
- AddOrganizationMembersRequest SDK type with user_ids field (max 100)
- postOrganizationMembers handler with batch GetUsersByIDs lookup,
  pre-filtered duplicate skipping, atomic InTx insert
- Proper error handling: 400 for missing users, 500 for server errors
- Audit logging via BackgroundAudit for each inserted member
- Returns 201 Created with only newly-added members

Frontend:
- API.addOrganizationMembers API method
- addOrganizationMembers query mutation
- OrganizationMembersPage wired to use the batch mutation
- Multi-user select dialog with search
- UsersFilter added to members page

Tests:
- TestAddMembers with 5 subtests: OK, AlreadyMember (skip),
  UserNotFound, SingleUser, SkipsDuplicates
@jakehwll jakehwll force-pushed the jakehwll/devex-112-organizations-batch-endpoint branch from 6bc2766 to 48724fd Compare May 1, 2026 01:15

@pawbana pawbana left a comment

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.

I'm not an expert in organizations but from what I could check LGTM (please add test for deleted user case 😅).
Maybe would be worth it to get review from someone more knowledgable about this part of the system.

Comment thread coderd/members.go
})
return
}
if len(deleted) > 0 {

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.

add test please

@github-actions github-actions Bot added the stale This issue is like stale bread. label May 12, 2026
@github-actions github-actions Bot closed this May 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release/breaking This label is applied to PRs to detect breaking changes as part of the release process stale This issue is like stale bread.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants