Error reference

last_owner_required

Declared 409, raised in the database retryable: false Postgres trigger

The membership change would remove or revoke the only remaining active owner of the workspace, leaving nobody able to administer it.

Why it happens

This is enforced by a BEFORE UPDATE OR DELETE trigger on workspace_memberships, not by application code. When a row with role owner and status active is deleted, or moved from active to revoked, the trigger looks for another active owner in the same workspace with a different identity. If there is none, it raises last_owner_required with SQLSTATE restrict_violation and the statement aborts. Because the rule lives in the database, it holds for every path that writes that table, including one that forgot to check.

Every condition below was read in the implementation at the file and line given. They are the branches that raise this code, not a summary of what the name suggests.

How to fix it

  1. Promote another member to owner first, then revoke the original. The trigger only needs one other active owner with a different identity to exist at the moment of the write.
  2. To wind a workspace down entirely, use the deletion job rather than removing the owner. Deletion is a lifecycle state on the workspace, not a membership change.
  3. Do not retry. The condition is structural and identical on every attempt.

Why it works this way

Placing this in a trigger rather than in a service method is the decision worth noting. An ownerless workspace is unrecoverable without operator intervention, so the guarantee is put where no code path can route around it, including a future one.

Messages you will see

The message field is written for a human reading a log and is not a contract value. Branch on code instead. These are the exact strings this code is sent with today, listed so that a search for one of them lands here.

Where it appears

This string is raised by a database trigger rather than by a route, so it reaches you through a database error or a log line rather than through the API. The surfaces below are the ones whose writes touch the guarded table. The REST base is https://api.emailforagents.ai/v1, and the remote MCP endpoint is https://mcp.emailforagents.ai/mcp.

Verified against supabase/migrations/20260919120400_rls_triggers.sql:50. The status comes from ERROR_STATUS in packages/contracts/src/errors.ts, which is the one place a code is mapped to a status.

All error codes · Documentation · Quickstart