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.
- Deleting, or revoking, the last workspace_memberships row with role owner and status active.
supabase/migrations/20260919120400_rls_triggers.sql:50 - Verified by integration test: revoking every owner membership for a workspace rejects with this string.
tests/integration/database.test.ts:38
How to fix it
- 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.
- 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.
- 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.
Workspace membership administration
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.