feat: enable RLS policies on all tables#38
Conversation
- games: public read, insert, update by ID; no delete - game_moves: public read, insert; no update/delete - game_snapshots: public read, insert; no update/delete - Grant anon role appropriate permissions - Add ADR-0004 documenting the decision
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughTwo new files establish Row Level Security (RLS) infrastructure for a Supabase-backed application. An ADR document specifies the security architecture decisions, while a database migration implements RLS policies on three tables with restrictive DELETE and UPDATE rules, alongside public SELECT and INSERT access. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
Updates to Preview Branch (feat/rls-policies) ↗︎
Tasks are run on every commit but only new migration files are pushed.
❌ Branch Error • Sun, 12 Apr 2026 13:21:24 UTC View logs for this Workflow Run ↗︎. |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
docs/adr/0004-rls-without-auth.md (1)
1-1: Minor wording polish: use “Row-Level Security”.Line 1 reads better as “Row-Level Security” (hyphenated compound modifier).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/adr/0004-rls-without-auth.md` at line 1, Update the ADR title line "# ADR-0004: Row Level Security without authentication" to hyphenate the compound modifier as "# ADR-0004: Row-Level Security without authentication" so it reads "Row-Level Security"; edit the heading text in the document (the existing title string) to use the hyphenated form.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/adr/0004-rls-without-auth.md`:
- Line 18: The ADR claims UPDATEs are restricted to ID-targeted, owner-only
updates but the migration creates a permissive CREATE POLICY "games_update" ON
games FOR UPDATE USING (true), which contradicts the doc; update either the ADR
text to reflect that updates are currently unrestricted or change the policy to
enforce ID/ownership checks (replace or drop the permissive "games_update"
policy and create a restrictive PERMISSIVE policy that checks the target id and
auth.uid(), or add a RESTRICTIVE policy instead of adding another permissive
policy) and remove the claim that UUID entropy provides access control;
reference the policy name "games_update", the existing USING (true) condition,
and auth.uid() checks when making the fix.
In `@supabase/migrations/20260412_rls_policies.sql`:
- Around line 7-22: The RLS policies (e.g., "games_select", "game_moves_insert",
"game_snapshots_no_delete") are missing explicit role scoping and currently
apply to all roles; update each CREATE POLICY statement to include TO anon so
they only apply to the anonymous/publishable key role (for all policies on
games, game_moves, and game_snapshots) by adding the TO anon clause for SELECT,
INSERT, UPDATE, and DELETE policies accordingly.
- Line 9: The "games_update" policy currently uses USING (true) which with the
GRANT ... UPDATE ON games TO anon allows anonymous clients to update any row;
replace the unconditional USING clause with a row-level ownership check (e.g.,
USING (auth.uid() = owner_id)) and add a matching WITH CHECK clause to ensure
updates only affect rows owned by the caller; update the CREATE POLICY
"games_update" definition (and keep the GRANT ... UPDATE ON games TO anon) so
anonymous users can only update rows where auth.uid() equals the row's owner
identifier.
---
Nitpick comments:
In `@docs/adr/0004-rls-without-auth.md`:
- Line 1: Update the ADR title line "# ADR-0004: Row Level Security without
authentication" to hyphenate the compound modifier as "# ADR-0004: Row-Level
Security without authentication" so it reads "Row-Level Security"; edit the
heading text in the document (the existing title string) to use the hyphenated
form.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6187ec0b-2942-4481-8f37-838641bd9e50
📒 Files selected for processing (2)
docs/adr/0004-rls-without-auth.mdsupabase/migrations/20260412_rls_policies.sql
| **Policies:** | ||
| - `SELECT` — allow all (highscores are public) | ||
| - `INSERT` — allow all (anyone can start a game) | ||
| - `UPDATE` — allow only where the request targets an existing game by ID (can only update your own active game) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
How are multiple PostgreSQL RLS policies combined for the same command, and can adding a stricter policy (e.g., auth.uid() check) still leave access open if an existing permissive policy remains?
💡 Result:
Multiple PostgreSQL RLS policies for the same command are combined as follows: All applicable PERMISSIVE policies (the default) are OR-ed together to determine granted access. All applicable RESTRICTIVE policies are AND-ed together as additional mandatory checks. A row is accessible only if at least one PERMISSIVE policy grants access (i.e., the OR of permissive expressions is true) AND all RESTRICTIVE policies pass (the AND of restrictive expressions is true). If no PERMISSIVE policies apply, access is denied by default. Adding a stricter policy like auth.uid = user_id (assuming auth.uid is a custom function returning the authenticated user ID) as a new PERMISSIVE policy will OR with existing PERMISSIVE policies, so it expands access rather than restricting it—rows passing either the new or existing permissive policy will be visible (subject to any restrictive policies). To tighten access, the new policy must be RESTRICTIVE (e.g., CREATE POLICY strict_policy ON table AS RESTRICTIVE USING (auth.uid = user_id)), which adds an AND check that all rows must pass, or the broad permissive policy must be dropped/replaced.
Citations:
- 1: https://www.postgresql.org/docs/current/ddl-rowsecurity.html
- 2: https://www.postgresql.org/docs/current/sql-createpolicy.html
🏁 Script executed:
cat -n docs/adr/0004-rls-without-auth.mdRepository: riethmayer/eureka
Length of output: 2510
🏁 Script executed:
# Find SQL files that might contain RLS policies
fd -e sql -e ts -e js -e py | head -20Repository: riethmayer/eureka
Length of output: 569
🏁 Script executed:
# Search for RLS policies related to games
rg -i "create policy|auth\.uid|UPDATE.*game" --max-count 20Repository: riethmayer/eureka
Length of output: 2469
🏁 Script executed:
cat -n supabase/migrations/20260412_rls_policies.sqlRepository: riethmayer/eureka
Length of output: 1728
Line 18 contradicts the actual SQL policy: the ADR promises ID-targeted updates only, but the implementation allows all updates.
The ADR states UPDATE — allow only where the request targets an existing game by ID (can only update your own active game) (line 18), but the SQL migration creates CREATE POLICY "games_update" ON games FOR UPDATE USING (true); (line 9 of supabase/migrations/20260412_rls_policies.sql), which permits updates on any row without ID or ownership checks.
Line 26's upgrade path is misleading. The ADR suggests that adding auth.uid() checks later will simply tighten policies, but PostgreSQL OR-s multiple PERMISSIVE policies together. A new auth.uid() PERMISSIVE policy will expand rather than restrict access—rows passing either the new check or the existing USING (true) policy will remain accessible. Tightening requires replacing the broad policy or adding a RESTRICTIVE policy instead.
Line 38 overstates UUID entropy as a security guarantee. UUIDs don't need to be guessed; they can be discovered in logs, client code, or shared between players. UUID unpredictability is not an access control mechanism.
Either align the ADR with the current permissive behavior (all updates allowed) or replace the games_update policy with actual ID-based access control.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/adr/0004-rls-without-auth.md` at line 18, The ADR claims UPDATEs are
restricted to ID-targeted, owner-only updates but the migration creates a
permissive CREATE POLICY "games_update" ON games FOR UPDATE USING (true), which
contradicts the doc; update either the ADR text to reflect that updates are
currently unrestricted or change the policy to enforce ID/ownership checks
(replace or drop the permissive "games_update" policy and create a restrictive
PERMISSIVE policy that checks the target id and auth.uid(), or add a RESTRICTIVE
policy instead of adding another permissive policy) and remove the claim that
UUID entropy provides access control; reference the policy name "games_update",
the existing USING (true) condition, and auth.uid() checks when making the fix.
| CREATE POLICY "games_select" ON games FOR SELECT USING (true); | ||
| CREATE POLICY "games_insert" ON games FOR INSERT WITH CHECK (true); | ||
| CREATE POLICY "games_update" ON games FOR UPDATE USING (true); | ||
| CREATE POLICY "games_no_delete" ON games FOR DELETE USING (false); | ||
|
|
||
| -- game_moves: public read, insert allowed, no update/delete | ||
| CREATE POLICY "game_moves_select" ON game_moves FOR SELECT USING (true); | ||
| CREATE POLICY "game_moves_insert" ON game_moves FOR INSERT WITH CHECK (true); | ||
| CREATE POLICY "game_moves_no_update" ON game_moves FOR UPDATE USING (false); | ||
| CREATE POLICY "game_moves_no_delete" ON game_moves FOR DELETE USING (false); | ||
|
|
||
| -- game_snapshots: public read, insert allowed, no update/delete | ||
| CREATE POLICY "game_snapshots_select" ON game_snapshots FOR SELECT USING (true); | ||
| CREATE POLICY "game_snapshots_insert" ON game_snapshots FOR INSERT WITH CHECK (true); | ||
| CREATE POLICY "game_snapshots_no_update" ON game_snapshots FOR UPDATE USING (false); | ||
| CREATE POLICY "game_snapshots_no_delete" ON game_snapshots FOR DELETE USING (false); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In PostgreSQL CREATE POLICY, what role(s) does a policy apply to when the TO clause is omitted?
💡 Result:
When the TO clause is omitted in a PostgreSQL CREATE POLICY statement, the policy applies to all roles (i.e., PUBLIC).
Citations:
- 1: https://www.postgresql.org/docs/current/sql-createpolicy.html
- 2: https://www.postgresql.org/docs/15/sql-createpolicy.html
- 3: https://www.postgrespro.com/docs/postgrespro/16/sql-createpolicy.html
- 4: http://www.postgres.com/docs/13/sql-createpolicy.html
- 5: https://www.postgresql.org/docs/10/sql-createpolicy.html
Scope policies explicitly to anon to avoid unintended role coverage.
Lines 7-22 omit TO anon, so these policies apply to all roles instead of just the anonymous role. If your access model relies on publishable-key/anon scoping, add the role target to each policy.
Example adjustment
-CREATE POLICY "games_select" ON games FOR SELECT USING (true);
+CREATE POLICY "games_select" ON games FOR SELECT TO anon USING (true);(Apply similarly to the other policies.)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@supabase/migrations/20260412_rls_policies.sql` around lines 7 - 22, The RLS
policies (e.g., "games_select", "game_moves_insert", "game_snapshots_no_delete")
are missing explicit role scoping and currently apply to all roles; update each
CREATE POLICY statement to include TO anon so they only apply to the
anonymous/publishable key role (for all policies on games, game_moves, and
game_snapshots) by adding the TO anon clause for SELECT, INSERT, UPDATE, and
DELETE policies accordingly.
| -- games: public read, anyone can insert, update only by ID, no delete | ||
| CREATE POLICY "games_select" ON games FOR SELECT USING (true); | ||
| CREATE POLICY "games_insert" ON games FOR INSERT WITH CHECK (true); | ||
| CREATE POLICY "games_update" ON games FOR UPDATE USING (true); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In PostgreSQL Row Level Security, does CREATE POLICY ... FOR UPDATE USING (true)allow a role to update all rows visible to that role, even if the client does not includeWHERE id = ...?
💡 Result:
Yes, in PostgreSQL Row Level Security, a policy CREATE POLICY ... FOR UPDATE USING (true) allows a role to update all rows that are visible to that role (i.e., all rows that pass the combined SELECT policies), even if the client issues an UPDATE without a WHERE clause (like UPDATE table SET col = value). The FOR UPDATE USING (true) policy means the UPDATE command can target any rows visible via SELECT policies. Without a WHERE clause, it targets exactly those visible rows. The update will succeed as long as: - The existing rows pass the UPDATE USING (true), which they do. - The new row values pass the WITH CHECK (implicitly the same USING expression, true). - The new row values pass any applicable SELECT policies (required internally for the update mechanism). If there are no restrictive SELECT policies (or they permit all updatable rows post-update), the mass update succeeds. Official docs confirm: "The USING expression determines which records the UPDATE command will see to operate against" (CREATE POLICY docs), and RLS policies are evaluated before user WHERE conditions on visible rows.
Citations:
- 1: https://www.postgresql.org/docs/current/ddl-rowsecurity.html
- 2: http://www.postgresql.org/docs/15/sql-createpolicy.html
- 3: https://www.postgresql.org/docs/current/sql-createpolicy.html
- 4: http://www.postgresql.org/docs/15/ddl-rowsecurity.html
games updates are unrestricted for anon role and allow broad changes without ID targeting.
Line 9 (USING (true)) combined with line 25 (GRANT ... UPDATE ON games TO anon) allows anonymous clients to update any row in the table, including unconditional bulk updates. The USING (true) policy has no ownership check, defeating the intended "update only by ID" behavior.
Suggested direction
- CREATE POLICY "games_update" ON games FOR UPDATE USING (true);
- GRANT SELECT, INSERT, UPDATE ON games TO anon;
+ -- If ownership/targeting is required, disable direct UPDATE until a verifiable ownership mechanism exists
+ -- (e.g., auth-based policy or per-game write token checked in RLS/RPC).
+ CREATE POLICY "games_no_update" ON games FOR UPDATE TO anon USING (false);
+ GRANT SELECT, INSERT ON games TO anon;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@supabase/migrations/20260412_rls_policies.sql` at line 9, The "games_update"
policy currently uses USING (true) which with the GRANT ... UPDATE ON games TO
anon allows anonymous clients to update any row; replace the unconditional USING
clause with a row-level ownership check (e.g., USING (auth.uid() = owner_id))
and add a matching WITH CHECK clause to ensure updates only affect rows owned by
the caller; update the CREATE POLICY "games_update" definition (and keep the
GRANT ... UPDATE ON games TO anon) so anonymous users can only update rows where
auth.uid() equals the row's owner identifier.
Summary
Security model
Test plan
pg_policiesSummary by CodeRabbit
Documentation
Chores