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

  1. Ensure the target user is an active member first: setActive or accept their invitation before setOwner.
  2. Pre-check: const m = await db.workspaceMember.findFirst({ where: { workspaceId, userId, state: 'active' } }); throw early if missing.
  3. Surface a clear error in the UI ('Invitee must accept and be active before ownership transfer').
  4. 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

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


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/af584a4ccbe08b62. Report an issue: GitHub.