Skip to content

feat: enable RLS policies on all tables#38

Merged
riethmayer merged 1 commit intomasterfrom
feat/rls-policies
Apr 12, 2026
Merged

feat: enable RLS policies on all tables#38
riethmayer merged 1 commit intomasterfrom
feat/rls-policies

Conversation

@riethmayer
Copy link
Copy Markdown
Owner

@riethmayer riethmayer commented Apr 12, 2026

Summary

  • Enable Row Level Security on games, game_moves, game_snapshots
  • Public read + insert, update by ID on games, no deletes anywhere
  • No service role key needed — publishable key only, scoped by RLS
  • ADR-0004 documents the decision

Security model

Table SELECT INSERT UPDATE DELETE
games all all by ID deny
game_moves all all deny deny
game_snapshots all all deny deny

Test plan

  • RLS policies applied and verified via pg_policies
  • Migration SQL checked in for reproducibility
  • Manual QA: play a game, verify saves still work with RLS enabled

Summary by CodeRabbit

  • Documentation

    • Added architecture decision documentation outlining database security policies and authorization rules.
  • Chores

    • Enabled Row Level Security on database tables, implementing granular access controls to restrict unauthorized data modifications.

- 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
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
eureka Ready Ready Preview, Comment Apr 12, 2026 1:21pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

Two 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

Cohort / File(s) Summary
Architecture Decision Record
docs/adr/0004-rls-without-auth.md
Documents ADR-0004 defining RLS policy intentions: broad SELECT and INSERT, restricted UPDATE on game records by ID, and denied DELETE operations using publishable key-only authentication.
Database Migration
supabase/migrations/20260412_rls_policies.sql
Enables RLS on games, game_moves, and game_snapshots tables; creates policies allowing public SELECT and INSERT, restricting UPDATE to games only, and denying DELETE on all tables; grants anon role appropriate permissions per table.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 With tables now guarded by policies so tight,
SELECT and INSERT hop freely in light,
While UPDATE and DELETE face walls in the night,
Our rows sit secure with RLS in sight!
No service keys needed, just public and pure,
A hopping good choice for safety and lure! 🔒✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat: enable RLS policies on all tables' directly and clearly summarizes the main change: enabling Row Level Security policies across the database tables.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rls-policies

Comment @coderabbitai help to get the list of available commands and usage tips.

@supabase
Copy link
Copy Markdown

supabase Bot commented Apr 12, 2026

Updates to Preview Branch (feat/rls-policies) ↗︎

Deployments Status Updated
Database Sun, 12 Apr 2026 13:21:15 UTC
Services Sun, 12 Apr 2026 13:21:15 UTC
APIs Sun, 12 Apr 2026 13:21:15 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Sun, 12 Apr 2026 13:21:22 UTC
Migrations Sun, 12 Apr 2026 13:21:23 UTC
Seeding ⏸️ Sun, 12 Apr 2026 13:21:00 UTC
Edge Functions ⏸️ Sun, 12 Apr 2026 13:21:00 UTC

❌ Branch Error • Sun, 12 Apr 2026 13:21:24 UTC

ERROR: relation "games" does not exist (SQLSTATE 42P01)
At statement: 0
-- Enable RLS on all tables
ALTER TABLE games ENABLE ROW LEVEL SECURITY

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between b08c1f0 and 4124913.

📒 Files selected for processing (2)
  • docs/adr/0004-rls-without-auth.md
  • supabase/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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


🏁 Script executed:

cat -n docs/adr/0004-rls-without-auth.md

Repository: 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 -20

Repository: 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 20

Repository: riethmayer/eureka

Length of output: 2469


🏁 Script executed:

cat -n supabase/migrations/20260412_rls_policies.sql

Repository: 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.

Comment on lines +7 to +22
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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:


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.

@riethmayer riethmayer merged commit 1cc418c into master Apr 12, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant