toeverything/AFFiNE · error · Error
New workspace owner must be an active member.
Error message
New workspace owner must be an active member.
What it means
Plain Error thrown by WorkspaceMemberModel.setOwner (packages/backend/server/src/models/permission-write.ts:141). setOwner takes a PostgreSQL advisory lock (pg_advisory_xact_lock on a hash of the workspace id), counts active owners, and — if at least one active owner exists — requires the target userId to be an active member of the workspace. If findFirst by (workspaceId, userId, state:'active') returns null, the guard throws. Because it is a plain Error, callers see an internal_server_error unless they validate first.
Source
Thrown at packages/backend/server/src/models/permission-write.ts:141
@Injectable()
export class WorkspaceMemberModel extends BaseModel {
@Transactional()
async setOwner(
workspaceId: string,
userId: string,
fallbackRole: WorkspaceRole
) {
await this.db
.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${`permission:workspace-owner:${workspaceId}`}, 0))`;
const ownerCount = await this.db.workspaceMember.count({
where: { workspaceId, role: 'owner', state: 'active' },
});
if (ownerCount > 0) {
const target = await this.db.workspaceMember.findFirst({
where: { workspaceId, userId, state: 'active' },
});
if (!target) {
throw new Error('New workspace owner must be an active member.');
}
}
await this.db.workspaceMember.updateMany({
where: {
workspaceId,
role: 'owner',
userId: { not: userId },
state: 'active',
},
data: {
role: workspaceRoleToNew(fallbackRole),
source: 'legacy',
},
});
return await this.db.workspaceMember.upsert({
where: {View on GitHub (pinned to 26c515e050)
Solutions
- Ensure the target user is an active member first: setActive or accept their invitation before setOwner.
- Pre-check: const m = await db.workspaceMember.findFirst({ where: { workspaceId, userId, state: 'active' } }); throw early if missing.
- Surface a clear error in the UI ('Invitee must accept and be active before ownership transfer').
- For first-owner setup (no existing owner), this guard is skipped — only the active-member path triggers it.
Example fix
// before
await memberModel.setOwner(workspaceId, newOwnerId, WorkspaceRole.Admin);
// after
const active = await db.workspaceMember.findFirst({ where: { workspaceId, userId: newOwnerId, state: 'active' } });
if (!active) throw new Error('Promote the user to active member first.');
await memberModel.setOwner(workspaceId, newOwnerId, WorkspaceRole.Admin); Defensive patterns
Strategy: validation
Validate before calling
const active = await db.workspaceMember.findFirst({
where: { workspaceId, userId, state: 'active' },
});
if (!active) {
throw new Error('Target user must be an active member before ownership transfer');
}
await memberModel.setOwner(workspaceId, userId, WorkspaceRole.Admin); Type guard
const isActiveMember = (m: { state: string } | null): m is { state: 'active' } =>
m !== null && m.state === 'active'; Try / catch
try {
await memberModel.setOwner(workspaceId, userId, fallback);
} catch (e) {
if (e instanceof Error && /active member/i.test(e.message)) {
ui.warn('Invitee must be an active member first.');
return;
}
throw e;
} Prevention
- Activate or invite-and-accept the target member before calling setOwner.
- Pre-check active membership to convert the plain Error into a clear UI message.
- Remember setOwner is serialized per workspace by an advisory lock.
When it happens
Trigger: Calling setOwner(workspaceId, userId, fallbackRole) when an active owner already exists but userId is not an active member (invited, pending, removed, or in a different workspace). The advisory lock serializes owner transfers per workspace, then the active-membership check fails.
Common situations: Transferring ownership to a user who was invited but never accepted; to a removed member; to a user from another workspace; race where the member is deactivated between the UI action and the call.
Related errors
- Cannot grant Owner role of a workspace to a user.
- owner_can_not_leave_workspace
- can_not_batch_grant_doc_owner_permissions
- failed_to_save_updates
- doc_history_not_found
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/af584a4ccbe08b62.
Report an issue: GitHub.