Skip to content

feat: Supabase auth, RLS backend and live leaderboard (M10) - #9

Merged
Amayyas merged 7 commits into
mainfrom
feat/m10-auth-backend
Jul 22, 2026
Merged

feat: Supabase auth, RLS backend and live leaderboard (M10)#9
Amayyas merged 7 commits into
mainfrom
feat/m10-auth-backend

Conversation

@Amayyas

@Amayyas Amayyas commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Delivers module M10 — Auth, Backend & Classement: the Supabase data model with row level security, accounts, route guards and the live worldwide leaderboard. Two commits.

Verified against a real database, not just written

I ran a local Supabase stack (Docker) and checked the whole chain against real Postgres, real auth and real Realtime:

Check Result
Sign-up creates the profile (trigger) ✅ username, avatar, level, date
Submitting your own score
Submitting a score under someone else's id 🔒 refused (403)
Renaming someone else's profile 🔒 refused
Writing someone else's puzzle progress 🔒 refused (403)
Deleting a score you submitted 🔒 refused — scores are final
Guest reading the leaderboard
Guest reading puzzle progress 🔒 refused

And in the browser: a guest visiting /leaderboard lands on the login screen, signing up through the UI creates the account and shows "Inscrit le 22/07/2026", and a score inserted from outside the browser appeared at the top of the board with no reload — Realtime works.

The bug that only a real database could show

The first run failed almost everywhere with 42501 permission denied, despite correct policies. RLS filters rows; a GRANT opens the table — and relying on Supabase's default privileges is not portable. Without explicit grants nothing worked at all. Had I shipped this without running it, the backend would have been dead on arrival.

What is in it

  • Schema: profiles, scores, puzzle_progress, achievements, RLS on every table with no permissive default, writes always pinned to auth.uid(), a sign-up trigger so an account can never exist without a profile, and scores in the realtime publication.
  • Auth: email sign-up/sign-in, Google OAuth, sign-out, session restored on load with a self-refreshing JWT, and Supabase's technical errors translated for the player.
  • Guard: RequireAuth on the leaderboard only — every game mode stays open to guests, as section 2.6 requires. It waits for the stored session before deciding, or a signed-in player would be bounced on every refresh.
  • Leaderboard: top ten per piece or overall, filtered by day/week/all time, live, with the signed-in player highlighted, and only each player's best round so one person cannot fill the table.
  • Hunt submits scores automatically when signed in and invites anonymous players who post one.
  • Profile: pseudonym, avatar among the six piece symbols, sign-up date, sign-out.

Notes and deviations

  • Google OAuth is wired as you asked, but the button only works once you enable the Google provider in your Supabase project (which also needs Google Cloud credentials). Everything else works without it.
  • Without credentials the app is unchanged: no account UI, guest play everywhere, and CI needs no secrets. Tests run deliberately unconfigured.
  • The leaderboard covers the four champions the Hunt actually offers; the spec listed the king, which you had removed from that mode.
  • The bundle budget moves from 200 to 230 kB. The Supabase client is ~65 kB gzipped and is a hard requirement here. I raised it with the reason recorded in the script rather than quietly; M9 should bring it back down with the route-level lazy loading the spec assigns to it.
  • To run it: copy .env.example to .env.local with your project URL and anon key, and apply supabase/migrations/.

npm run ci green: format, lint, typecheck, 245 tests, build, bundle budget.

Summary by CodeRabbit

  • New Features
    • Added login and registration flows (email/password plus Google sign-in) and a protected worldwide leaderboard.
    • Delivered a full leaderboard experience with live updates, piece/period filters, and polished empty/loading/error states.
    • Introduced an authenticated profile area with avatar-piece selection and sign-out.
  • Bug Fixes
    • Improved guest-mode behavior: leaderboard shows an availability guard when backend auth isn’t configured; updated test expectations accordingly.
  • Documentation
    • Documented optional backend setup and added environment examples for Supabase local use.
  • Tests & Chores
    • Added leaderboard hook tests; updated routing/navigation tests; adjusted bundle-size budget and Supabase-related ignore patterns.

Amayyas added 2 commits July 22, 2026 22:29
The data model of section 2.6 - profiles, scores, puzzle_progress and
achievements - as a migration, with RLS enabled on every table and no
permissive default. Section 06 rates a misconfigured RLS as a high impact
risk, so each table states exactly who may read and who may write.

- Profiles and scores are world-readable: the leaderboard has to show who
  holds a score, and guests are allowed to browse it.
- Writes are always pinned to auth.uid(), so a score can only ever be
  filed under its own author.
- Scores get no update or delete policy: a submitted score is final.
- Puzzle progress is private to its owner.
- A trigger creates the profile on sign-up, so an account can never exist
  without one, and it falls back to a suffixed username rather than
  failing the signup on a collision.
- scores joins the realtime publication for the live leaderboard.

Verified against a real local Postgres: signing up creates the profile,
and every attempt to write in someone else's name is refused (403), as is
deleting a submitted score, while a guest can still read the board.

The grants matter as much as the policies: RLS filters rows, a GRANT
opens the table. Without them every request failed with 42501 however
correct the policies were - which is exactly what the first run against a
real database showed.
Section 2.6, end to end.

- Supabase client that degrades to nothing when unconfigured, so the app
  stays fully playable as a guest and the build needs no secrets.
- useAuthStore: email sign-up and sign-in, Google OAuth, sign-out, the
  session restored on load with its JWT refreshing itself, and Supabase's
  technical errors translated for the player.
- Sign-in and sign-up screens, and a RequireAuth guard on the leaderboard
  only - every game mode stays open to guests. The guard waits for the
  stored session before deciding, or a signed-in player would be bounced
  to the login screen on every refresh.
- Leaderboard: top ten per piece or overall, filtered by day, week or all
  time, refreshed live through Supabase Realtime, with the signed-in
  player highlighted. Only each player's best round is listed, so one
  person cannot fill the table.
- Hunt submits a score automatically when signed in, and invites an
  anonymous player who posts one to create an account.
- Profile gains the account: pseudonym, avatar among the six piece
  symbols, sign-up date and sign-out.

Verified in a real browser against a local Supabase: a guest visiting the
leaderboard lands on the login screen, signing up creates the account and
profile and shows its date, and a score inserted from outside the browser
appears at the top of the board without a reload.

Tests run with the backend deliberately unconfigured, which is what CI
sees and keeps them off the network.

The initial JavaScript budget moves from 200 to 230 kB: the Supabase
client is a hard requirement of this module. M9 should bring it back down
with the route-level lazy loading the specification assigns to it.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8df2a3c1-9814-4c34-bd07-fd5374bdb147

📥 Commits

Reviewing files that changed from the base of the PR and between 00c0541 and f7afd5b.

📒 Files selected for processing (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md

📝 Walkthrough

Walkthrough

Adds optional Supabase integration with local configuration, authentication routes and state, protected leaderboard access, persistent authenticated scores, realtime leaderboard updates, and profile account controls. Guest mode remains available without Supabase credentials.

Changes

Supabase foundation

Layer / File(s) Summary
Supabase client, schema, and local configuration
.env.example, .gitignore, .prettierignore, package.json, src/lib/supabase.ts, supabase/*, scripts/check-bundle-size.mjs
Adds Supabase configuration, typed client data, local services, database tables and policies, signup profile creation, realtime scores, and an expanded bundle budget.

Authentication and routing

Layer / File(s) Summary
Authentication state and account routes
src/store/useAuthStore.ts, src/features/auth/*, src/App.tsx, src/routes.ts, vite.config.ts, src/App.test.tsx, src/components/Layout/navigation.test.ts
Adds session initialization, email/password and Google authentication, login and registration pages, protected leaderboard routing, guest-mode guards, and backend-disabled test configuration.

Score submission and leaderboard

Layer / File(s) Summary
Persistent scoring and leaderboard UI
src/features/hunt/HuntPage.tsx, src/features/leaderboard/*
Stores positive authenticated scores, prompts guests to register, queries and deduplicates leaderboard rows, refreshes on score inserts, and renders filtered leaderboard states.

Profile account controls

Layer / File(s) Summary
Account card and profile actions
src/features/profile/ProfilePage.tsx
Adds unavailable, guest, and signed-in account states with avatar selection, signup date, profile updates, and sign-out.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant useAuthStore
  participant RequireAuth
  participant LoginPage
  participant SupabaseAuth
  App->>useAuthStore: initialise()
  useAuthStore->>SupabaseAuth: restore session
  RequireAuth->>useAuthStore: check session
  RequireAuth->>LoginPage: redirect when unauthenticated
  LoginPage->>SupabaseAuth: submit credentials
  SupabaseAuth-->>useAuthStore: return session
  LoginPage-->>RequireAuth: navigate to leaderboard
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change set: Supabase auth, RLS backend, and a live leaderboard.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/m10-auth-backend

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

@Amayyas Amayyas self-assigned this Jul 22, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/hunt/HuntPage.tsx`:
- Around line 62-71: Update the round-completion effect in HuntPage so
recorded.current only gates the local recordHunt call, while remote Supabase
submission can retry when session becomes available. Submit the score once for
an authenticated player with a positive score, track remote submission
separately to prevent duplicates, and inspect the insert result so failures are
surfaced through the existing error-handling mechanism instead of being
discarded.

In `@src/features/profile/ProfilePage.tsx`:
- Line 64: Update the ProfilePage render state around the session/authProfile
condition to model profile loading and profile error separately from
authentication state. Ensure restored sessions remain in an authenticated
loading or error state while loadProfile() is pending or fails, and render guest
CTAs only when session === null; preserve the authenticated profile view once
authProfile is available.

In `@src/store/useAuthStore.ts`:
- Around line 51-62: Update loadProfile to clear the existing profile before
fetching, then commit the fetched profile only when the returned session user
still matches the store’s current session user. Prevent results from sign-out or
a different user session from overwriting the active profile.

In `@supabase/config.toml`:
- Around line 159-163: Update the local OAuth redirect configuration around
additional_redirect_urls to allow the exact http://127.0.0.1:3000/profile
callback used by signInWithOAuth(), or change redirectTo to match an existing
allow-listed URL; ensure local Google sign-in no longer uses the mismatched
HTTPS origin.
- Around line 319-335: Add an [auth.external.google] configuration block
alongside the existing OAuth provider settings, enabling Google with
environment-backed client_id and secret values, and configure the local redirect
URI as needed. Set skip_nonce_check only when required by the local client,
while preserving the provider’s default-disabled behavior unless explicitly
enabled.

In `@supabase/migrations/20260711000000_init.sql`:
- Around line 34-39: Restrict the authenticated update permissions for
public.profiles so clients can update only approved cosmetic columns, not
server-controlled xp or level. Update the profile UPDATE grant near the existing
“a user edits only their own profile” policy, while preserving the ownership
checks in that policy and ensuring server-side profile updates retain access to
xp and level.
- Around line 156-176: Update handle_new_user to make username allocation
atomic: replace the standalone profiles existence check with an insert-and-retry
flow that handles unique conflicts and derives another candidate until insertion
succeeds. In the same function, validate raw_user_meta_data.avatar_piece against
the allowed values ('k','q','r','b','n','p') and use 'n' for null or invalid
values before inserting. Preserve the existing username sanitization and
short-name fallback behavior.
🪄 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: 9449dfed-3c75-4026-93d7-757f9dafedc5

📥 Commits

Reviewing files that changed from the base of the PR and between a8215c6 and 56c6b5d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (24)
  • .env.example
  • .gitignore
  • .prettierignore
  • package.json
  • scripts/check-bundle-size.mjs
  • src/App.test.tsx
  • src/App.tsx
  • src/components/Layout/navigation.test.ts
  • src/features/auth/AuthLayout.tsx
  • src/features/auth/LoginPage.tsx
  • src/features/auth/RegisterPage.tsx
  • src/features/auth/RequireAuth.tsx
  • src/features/hunt/HuntPage.tsx
  • src/features/leaderboard/LeaderboardPage.tsx
  • src/features/leaderboard/useLeaderboard.test.ts
  • src/features/leaderboard/useLeaderboard.ts
  • src/features/profile/ProfilePage.tsx
  • src/lib/supabase.ts
  • src/routes.ts
  • src/store/useAuthStore.ts
  • supabase/.gitignore
  • supabase/config.toml
  • supabase/migrations/20260711000000_init.sql
  • vite.config.ts

Comment thread src/features/hunt/HuntPage.tsx Outdated
Comment thread src/features/profile/ProfilePage.tsx
Comment thread src/store/useAuthStore.ts
Comment thread supabase/config.toml Outdated
Comment thread supabase/config.toml
Comment thread supabase/migrations/20260711000000_init.sql
Comment thread supabase/migrations/20260711000000_init.sql
Amayyas added 4 commits July 22, 2026 23:02
Row Level Security filters rows, not columns, so the ownership policy on
profiles left every column of one's own row writable — including xp and
level, which a player could set to anything with a single PATCH. Section
06 of the specification rates a misconfigured RLS as a high impact risk.

Grant update on (username, avatar_piece) only, and insert on the three
columns a client legitimately supplies. Verified against a local stack:
xp and level now return 42501 while a username change still succeeds.

Also harden the signup trigger, which runs before any of this: coerce an
unknown avatar_piece from the client metadata to the default instead of
letting the check constraint abort the whole signup, and retry on a
username collision rather than checking for one first, since two
concurrent signups could both pass that check and one would then fail.
One flag gated both the local record and the worldwide submission, so a
round that ended before the stored session had been restored recorded
the score locally, marked itself done and never submitted it. Track the
two separately: the submission effect simply runs again when the session
lands.

The insert result was also discarded, which made a rejected score look
like a broken leaderboard. Report the failure and allow a later retry.
The profile fetch had no ordering guarantee, so signing out and back in
could let the first, slower response overwrite the new session's profile.
Discard responses that a newer request has superseded.

The profile page fell through to the guest block while the session and
profile were still loading, inviting a signed-in player to create a
second account; it now says it is loading.

Local Google sign-in could not work either: the config declared no
google provider, and the redirect allow-list held neither the /profile
callback the client asks for nor the port this app actually serves on.
Section 2.6 needs a Supabase project, and nothing so far said how to make
one. Walk through the project, the schema, the redirect URLs and the
Google provider, and state plainly that all of it is optional: without a
backend the app stays fully playable as a guest.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 104-107: Add the local Supabase OAuth callback URL
http://127.0.0.1:54321/auth/v1/callback alongside the hosted callback in the
Google OAuth client setup instructions, preserving the existing redirect URI
guidance.
🪄 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: bce8e2f7-589b-45cd-85fd-182a4828a4d3

📥 Commits

Reviewing files that changed from the base of the PR and between b698c0c and 00c0541.

📒 Files selected for processing (2)
  • README.md
  • supabase/.env.example

Comment thread README.md Outdated
config.toml points the local stack at its own auth service on port 54321,
but the guide only asked for the hosted callback, so Google would reject
a local sign-in with redirect_uri_mismatch.
@Amayyas
Amayyas merged commit e32247b into main Jul 22, 2026
6 checks passed
@Amayyas
Amayyas deleted the feat/m10-auth-backend branch July 22, 2026 21:51
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