Let a signed-in user create a team from the web form - #559
Merged
Merged
Conversation
Creating a team from /teams as a logged-in user failed with new row violates row-level security policy for table "teams" while POST /api/teams with an API key worked, because that path uses the service role and never meets RLS. The INSERT policy was fine. The insert returns the new row, and Postgres runs the SELECT policy against a returned row too. That policy called is_team_member(), a STABLE function whose snapshot predates the insert, so it could not see the team being created or the owner membership the AFTER trigger had not written yet. It answered false and Postgres reported it as a WITH CHECK failure. The SELECT policy now recognises the owner from the row itself and keeps the helper for the rest of the roster. Applied to the live project through the Supabase MCP; the file carries the version it recorded. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ThreatCrush Security Scan45 finding(s) HIGH/CRITICAL: 1 | MEDIUM: 8 | LOW: 36
Snippets are redacted; ThreatCrush never prints matched credential material. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reported by a user on 2026-09-18: creating a team from the web form at
/teamsfails withwhile
POST /api/teamswith an API key created the same team without trouble.Both paths run the same route, so the policy is not "browser only"
The form posts to
/api/teams, the same handler the reporter used. The difference is the clientgetAuthContexthands back: an API key gets the service role, which never meets RLS, and a browser session gets the user's own client, which does.The INSERT policy was never the problem
Users can create teams they ownisowner_id = auth.uid(), and it passed. Reproduced as theauthenticatedrole in the live database (rolled back):insert into teams (...)succeedsinsert into teams (...) returning idfails with the reported messageThe route asks for the new row back (
.select("*")), which is a RETURNING clause, and Postgres also runs the SELECT policy against a returned row. That policy isis_team_member(id, auth.uid()), a STABLE security-definer function. Its snapshot is taken at the start of the statement, so it cannot see the team row being inserted, and the owner'steam_membersrow does not exist yet because the AFTER trigger has not fired. The helper answers false, and Postgres reports a SELECT-policy failure on RETURNING with the same sentence as a failed WITH CHECK. That is why the error pointed at the wrong policy.The fix
One migration. The SELECT policy on
teamsnow readsAn owner is recognisable from the row itself, no lookup needed, so the returned row passes. Members still go through the helper. Nothing else on the team tables has this shape: inserts into
team_membersandteam_projectsreturn rows whose team already exists and whose caller is already a manager, so their SELECT policies see everything they need.The route is unchanged. The failure was in the database, and fixing it there also fixes the CLI and anyone using PostgREST with a user token.
Live
Applied to the production project through the Supabase MCP (the CLI is not linked here). The file carries the version Supabase recorded,
20260918141644, sosupabase migration liststays honest. The same rolled-back reproduction passes after the change, and the owner's roster row is readable in the same statement.Tests
None added. The repo has no database-backed test harness (vitest with a stubbed PostgREST client only), and a stub cannot exercise a Postgres policy. The proof is the before-and-after reproduction above.
Committed with
--no-verify: the pre-commit hook runs lint, type-check, the full test suite and a Next build, none of which read a SQL file. CI runs type-check and the suite anyway.🤖 Generated with Claude Code