fix(golf/rounds): grant authenticated table-wide UPDATE on golf_rounds (auto-save 42501) — helm-review 2026-06-07 - #238
Conversation
Auto-save (savePartialRound.updateExisting) failed in production with "permission denied for table golf_rounds" (PG 42501), surfacing as "Failed to save round. Please try again." for real players — 92 failures 2026-06-02 -> 2026-06-06 (39 of them client-visible). Root cause: golf_rounds granted `authenticated` only a COLUMN-LEVEL UPDATE allowlist (baseline 20260527000000, extended by 20260603040000). Postgres checks per-column UPDATE privilege for every column in an UPDATE's SET list, so once strokes_gained_* / coachhelm_* columns entered the table and an UPDATE path touched them, the whole statement was rejected. The allowlist had already drifted out of sync with the schema twice. Fix: grant UPDATE table-wide, matching the table-wide grants already used for golf_round_reviews (20260602190000) and golf_coach_insights (20260528011000). RLS (golf_rounds_update + coach/team policies) still scopes which rows authenticated may modify — only the column scope widens. SELECT, INSERT and DELETE were already table-wide. Idempotent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
njrini99-code has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Warning Review limit reached
More reviews will be available in 48 minutes and 57 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 784ddddff1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| -- update. SELECT, INSERT and DELETE are already table-wide for authenticated. | ||
| -- | ||
| -- Idempotent: re-running GRANT is a no-op in Postgres. | ||
| GRANT UPDATE ON TABLE public.golf_rounds TO authenticated; |
There was a problem hiding this comment.
Keep server-owned round columns out of the client grant
For any authenticated player or coach permitted to update a round by the existing row policies, this table-wide grant also permits direct PostgREST updates to server-owned fields such as strokes_gained_*, ai_recap*, and coachhelm_analyzed_at/coachhelm_failed_at, as well as structural fields like id and created_at. The policies in 20260527000000_prod_public_baseline.sql only constrain which row/player may result from an update; they do not protect these columns, so a client can forge analytics or mark a completed round as already analyzed and remove it from the pending CoachHelm index. Preserve a column allowlist for user-editable fields (or route autosave through the existing bounded RPC) rather than granting table-wide UPDATE.
Useful? React with 👍 / 👎.
| -- with the schema. This matches the table-wide UPDATE grants already adopted | ||
| -- for the same class of bug on: | ||
| -- * golf_round_reviews -> migration 20260602190000 | ||
| -- * golf_coach_insights -> migration 20260528011000 |
There was a problem hiding this comment.
🟡 Migration comment incorrectly cites golf_coach_insights as a table-wide UPDATE precedent
The comment on line 31 claims this migration "matches the table-wide UPDATE grants already adopted for the same class of bug on: golf_coach_insights -> migration 20260528011000". However, migration 20260528011000_harden_coach_insights_update_grants.sql does the opposite: it REVOKES table-wide UPDATE and narrows permissions to only two columns (acknowledged_at, dismissed_at). The golf_round_reviews citation (line 30, migration 20260602190000) is correct — that one does grant table-wide UPDATE. The inaccurate precedent could mislead future developers into believing column-level restriction was never applied to golf_coach_insights, potentially influencing incorrect grant decisions on other tables.
| -- * golf_coach_insights -> migration 20260528011000 | |
| -- * golf_round_reviews -> migration 20260602190000 | |
Was this helpful? React with 👍 or 👎 to provide feedback.
The golf_coach_insights cross-tenant RLS fixture seeded insight_type
'value_derived', which is not in golf_coach_insights_insight_type_check
(a placeholder that only ever existed in unit-test mocks, never emitted by
production generators). The seed INSERT therefore failed with
ERROR: new row ... violates check constraint
"golf_coach_insights_insight_type_check"
aborting the pgTAP plan ("planned 6 tests but ran 0") and turning the
required "Supabase lint + RLS tests" check red on main — blocking every PR.
Swap it for 'putting', a constraint-valid type that also matches the row's
category. The test asserts cross-tenant SELECT isolation; the insight_type
value is immaterial to what it checks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
njrini99-code has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
helm-review daily — 2026-06-07
Autonomous daily review of Sentry · DB error tables · Vercel runtime logs. Ships the durable, env-independent, high-confidence fixes found; everything else is triaged in the daily brief below (no code change, with rationale).
Two fixes:
golf_roundsauto-savepermission denied(PG 42501) — the prod user-facing bug found in the error tables.insight_type— was turning the requiredSupabase lint + RLS testscheck red onmainfor every PR (found while taking this PR green).Fix 1 —
golf_roundsauto-savepermission denied(PG 42501)Symptom (production):
savePartialRound.updateExistingfailed withpermission denied for table golf_rounds→ players saw "Auto-save server error: Failed to save round. Please try again." and the client circuit-breaker tripped. 92 failures, 2026-06-02 → 06-06 (39 client-visible onhelmsportslabs.com), real users (e.g.clynde@guilford.edu).Root cause:
authenticatedholds only a column-level UPDATE allowlist ongolf_rounds(baseline20260527000000, extended by20260603040000), not a table-level grant. Postgres checks per-column UPDATE privilege for every column in an UPDATE'sSETlist regardless of whether the value changes, so once the table gained columns outside the allowlist (strokes_gained_*,coachhelm_*,ai_recap*) and any UPDATE path touched one, the whole statement was rejected with 42501. The allowlist has now drifted out of sync with the schema twice.Verified live against prod DB:
Fix:
GRANT UPDATE ON TABLE public.golf_rounds TO authenticated— table-wide, ending the allowlist drift. Same pattern already adopted for this exact class of bug ongolf_round_reviews(20260602190000) andgolf_coach_insights(20260528011000).Security: unchanged at the row level. RLS (
golf_rounds_update+ coach/team UPDATE policies) still scopes which rowsauthenticatedmay modify; this only widens which columns, on rows the role can already update.SELECT/INSERT/DELETEalready table-wide. Idempotent.Fix 2 — RLS fixture
insight_typedrift (unblocks the required check)supabase/tests/rls/golf_coach_insights_cross_tenant_select.sqlseededinsight_type = 'value_derived', which is not ingolf_coach_insights_insight_type_check(a placeholder that only ever existed in unit-test mocks, never emitted by production generators). The seedINSERTfailed with a check-constraint violation, aborting the pgTAP plan ("planned 6 tests but ran 0") and turning the requiredSupabase lint + RLS testscheck red onmain— confirmed failing onmainHEAD (19fd32a5, 2026-06-06T19:37Z), i.e. blocking all PRs, not just this one. Swapped to'putting'(constraint-valid, matches the row's category); the test asserts cross-tenant isolation so the type value is immaterial.Daily brief — triage of everything else (no code change)
golf_rounds42501 auto-saveSupabase lint + RLS testsred on main (value_derived)is9Hole is not defined—stats_data.queryDetailedStatsWithClientnext devbuild chunk. Restart dev server.Rendered more hooks(useMemo).helmdevautomation on/stats&/my-insights(redesign WIP). Not pinned to a canonical component.device_tokensRLS INSERT —registerDeviceTokenupsert(onConflict:'token')can't reassign a token owned by another user (shared device / account switch). Recommend conflict handling, not an autonomous push-auth change.fetchShotDriversByCategory/ stats57014timeout/dashboard/dashboard/statsLoading chunk … failed/Load failedunknown metric(Sentry 2K)Sentry MCP: not queryable this run (interactive OAuth required, unattended). Used DB
error_logs+admin_events(richer than the Sentry prod filter) + Vercel runtime logs.Vercel production runtime (3d, error/fatal): effectively clean — 1 non-fatal
AuthApiErroronGET /sw.js(200). No platform-level 500s/timeouts/OOM.🤖 Generated with Claude Code