diff --git a/app/features/tournament/actions/to.$id.register.server.ts b/app/features/tournament/actions/to.$id.register.server.ts
index a20c9037b..1f220b7a6 100644
--- a/app/features/tournament/actions/to.$id.register.server.ts
+++ b/app/features/tournament/actions/to.$id.register.server.ts
@@ -144,6 +144,10 @@ export const action: ActionFunction = async ({ request, params }) => {
}
case "DELETE_TEAM_MEMBER": {
errorToastIfFalsy(ownTeam, "You are not registered to this tournament");
+ errorToastIfFalsy(
+ !tournament.isInvitational,
+ "The organizer manages the roster of an invitational team",
+ );
errorToastIfFalsy(
ownTeam.memberUserIds.includes(data.userId),
"User is not in your team",
@@ -344,6 +348,10 @@ export const action: ActionFunction = async ({ request, params }) => {
}
case "UNREGISTER": {
errorToastIfFalsy(ownTeam, "You are not registered to this tournament");
+ errorToastIfFalsy(
+ !tournament.isInvitational,
+ "The organizer manages the roster of an invitational team",
+ );
errorToastIfFalsy(
!ownTeamCheckedIn,
"You cannot unregister after checking in",
diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx
index 0f5984f0d..9bd30bf26 100644
--- a/app/features/tournament/routes/to.$id.register.tsx
+++ b/app/features/tournament/routes/to.$id.register.tsx
@@ -180,19 +180,12 @@ function RegistrationForms({ readOnly = false }: { readOnly?: boolean }) {
const ownTeamCheckedIn = Boolean(ownTeam && ownTeam.checkIns.length > 0);
const hasFriendCodeSet = Boolean(user?.friendCode);
- if (!user && !tournament.isInvitational) {
+ if (!user) {
return ;
}
- const showRegistrationProgress = () => {
- if (ownTeam) return true;
-
- return !tournament.isInvitational;
- };
-
const showRegisterNewTeam = () => {
if (ownTeam) return true;
- if (tournament.isInvitational) return false;
if (!tournament.registrationOpen) return false;
return !tournament.regularCheckInHasEnded;
@@ -202,24 +195,19 @@ function RegistrationForms({ readOnly = false }: { readOnly?: boolean }) {
{showRegisterNewTeam() ?
: null}
{hasFriendCodeSet ? (
- showRegistrationProgress() ? (
-
- ) : (
-
- This tournament is invitational. Tournament organizer adds all
- teams.
-
- )
+
) : null}
{showRegisterNewTeam() && hasFriendCodeSet ? (
) : null}
{tournament.isLeague &&
@@ -682,6 +670,7 @@ function FillRoster({
const showDeleteMemberSection =
!readOnly &&
+ !tournament.isInvitational &&
((!ownTeamCheckedIn && ownTeamMembers.length > 1) ||
(ownTeamCheckedIn &&
ownTeamMembers.length > tournament.minMembersPerTeam));
diff --git a/changelog/2026-08-29-invitational-roster-kick.md b/changelog/2026-08-29-invitational-roster-kick.md
new file mode 100644
index 000000000..52ed88369
--- /dev/null
+++ b/changelog/2026-08-29-invitational-roster-kick.md
@@ -0,0 +1,5 @@
+---
+navItem: calendar
+type: bug
+---
+Captains of an invitational tournament team can no longer kick members or unregister the team on their own
diff --git a/e2e/pages/tournament/tournament-register-page.ts b/e2e/pages/tournament/tournament-register-page.ts
index 9b1e6166c..1e85f06b6 100644
--- a/e2e/pages/tournament/tournament-register-page.ts
+++ b/e2e/pages/tournament/tournament-register-page.ts
@@ -28,6 +28,8 @@ export class TournamentRegisterPage {
"Registration for this tournament has closed",
),
leaveTeamButton: page.getByRole("button", { name: "Leave the team" }),
+ deleteMemberButton: page.getByRole("button", { name: "Delete member" }),
+ unregisterButton: page.getByRole("button", { name: "Unregister" }),
organizerAddedLeaveExplanation: page.getByText(
"You were added to the team by the organizer. Contact the TO to leave the team.",
),
diff --git a/e2e/tournament-invitational.spec.ts b/e2e/tournament-invitational.spec.ts
index 3b960bc8f..e1d21a777 100644
--- a/e2e/tournament-invitational.spec.ts
+++ b/e2e/tournament-invitational.spec.ts
@@ -85,6 +85,13 @@ test.describe("Invitational tournament", () => {
await expect(
register.locators.organizerAddedLeaveExplanation,
).toBeVisible();
+
+ // nor can the captain kick them, the organizer owns the roster
+ await impersonate(page, captain.id);
+ await register.goto(tournament.id);
+ await expect(register.member(2)).toBeVisible();
+ await expect(register.locators.deleteMemberButton).toHaveCount(0);
+ await expect(register.locators.unregisterButton).toHaveCount(0);
});
});