Skip to content

fix(ratings): stop naming a user_id column production never had - #44

Merged
AnxForever merged 2 commits into
mainfrom
fix/ugc-user-identity-binding
Sep 20, 2026
Merged

AnxForever merged 2 commits into
mainfrom
fix/ugc-user-identity-binding

Conversation

@AnxForever

@AnxForever AnxForever commented Sep 20, 2026

Copy link
Copy Markdown
Owner

Summary

What changed:

Rating writes have returned 503 for every signed-in user since 2026-09-04. Production's style_ratings table never received migration 003's user_id half — only style_comments and submissions got theirs — so every rating lives under the legacy session_id = "user:<uuid>" identity.

1c3a4c42 collapsed the route's two insert shapes into one to dodge a supabase-js union-typing error. The legacy arm then sent user_id: null even after its own probe found the column missing. PostgREST rejects an insert that names a column it does not have rather than ignoring it, so the fallback the code was written around could never have worked.

Two migrations apply the missing halves and fold the legacy rows onto the real identity:

  • 041_style_ratings_user_binding.sql — 11 ratings, 9 users
  • 042_user_favorites_user_binding.sql — 370 favorites, 64 users

Why:

Without the code change, the next database that reaches this state breaks identically. Without the backfill, adding the column would make .eq("user_id", ...) — the read path the code prefers — return nothing, silently hiding every existing rating and favorite.

Two details deliberately differ from migration 003 as written, both established by direct test rather than reading:

  • The unique index is not partial. Postgres refuses to infer a partial index as the arbiter for the merge path's onConflict: "user_id,style_slug" and fails it with 42P10, which neither error classifier in the favorites route treats as a missing column. Dropping the predicate is free: Postgres treats NULL user_id values as distinct, so anonymous rows stay unconstrained.
  • The session_id NOT NULL drop is load-bearing. The modern arm inserts {user_id, style_slug} and never names session_id.

RLS is left alone on purpose. Every favorites and ratings read/write goes through the service role, and migration 034's policies key off session_id, which the backfill preserves. Rows the modern arm writes carry a NULL session_id, which those policies deny to anon callers — fail-closed.

Change Type

  • fix — bug fix

Scope

  • API Endpoints
  • Build / CI

Validation

  • pnpm run security:secrets — no secrets detected
  • pnpm run lint — no errors
  • npx tsc --noEmit — no type errors
  • pnpm test — 264 files / 7797 tests pass
  • pnpm build — builds successfully

The new route test was run against the unfixed code first and confirmed to fail on the not.toHaveProperty("user_id") assertion.

Security

  • No secrets, credentials, or .env files committed
  • Server-side values are not exposed via NEXT_PUBLIC_

Breaking Changes

  • None

Notes for Reviewers

Key files: app/api/styles/[slug]/rate/route.ts (the fix), the two migrations, components/styles/style-rating.tsx (the client stopped discarding the route's user-safe error, which is what kept a 16-day outage looking generic).

Verification already performed against production, since the failure only occurs past authentication:

  • Backfill equivalence checked per user: users_lost_or_changed = 0 across all 64 favorites owners.
  • A temporary account signed in through the real password flow and posted to the live endpoint: {"success":true,...,"userRating":4} HTTP 200, re-rating updated in place without a second row, and the row landed with user_id set and session_id NULL. The account and its rating were deleted afterwards; row counts returned to 11 ratings / 371 favorites.
  • Migrations were dry-run inside rolled-back transactions, including a second application to confirm idempotency, before being applied.

Known risk: the migrations are already applied in production, so the schema leads this PR. Merging only brings the source of truth back in line.

Deploy note: pnpm build OOMs at node's default 2096 MB heap during the inlined TypeScript check (npx tsc --noEmit passes independently). NODE_OPTIONS="--max-old-space-size=6144" is required locally.

Summary by CodeRabbit

  • New Features

    • Ratings and favorites can now be associated with authenticated user accounts, improving consistency across sessions and devices.
    • Existing valid account-linked data is preserved during the transition, while legacy session-based data remains supported.
    • Duplicate account-linked ratings and favorites are consolidated.
  • Bug Fixes

    • Rating submissions now provide clearer server error messages and refresh displayed rating data after failures.
    • Rating saves remain compatible with databases using the legacy schema.

Rating writes have been failing with 503 since 1c3a4c4 (2026-09-04), which
collapsed the two insert shapes into one. The legacy arm then sent
`user_id: null` even after its probe found the column missing, and PostgREST
rejects an insert that names an unknown column rather than ignoring it. The
route's own legacy fallback could never have worked.

Production's style_ratings table never received migration 003's user_id half,
so every rating lives under the `session_id = "user:<uuid>"` identity. The
insert now branches again, with an explicit payload type so both arms stay
assignable without the union that motivated the collapse.

Migration 041 applies the missing column and folds the existing rows onto it.
The client also stops discarding the route's already user-safe error message,
which is what kept a silent 16-day outage looking like a generic failure.
Migration 034 left 003's user_favorites.user_id column unapplied on the
grounds that no code referenced it. The code has referenced it since
2026-02-21 -- both the favorites API and the merge path try the user_id arm
first and fall back to the session identity -- so production has served every
signed-in write through the fallback.

Migration 042 applies the column and folds the 370 legacy rows onto it,
preserving their session_id so migration 034's RLS policies keep protecting
them unchanged. Two details deliberately differ from 003 as written: the
session_id NOT NULL drop is load-bearing, since the modern arm never names
that column, and the unique index is not partial, because Postgres refuses to
infer a partial index as the arbiter for the merge path's
onConflict "user_id,style_slug" and fails it with 42P10.
@coderabbitai

coderabbitai Bot commented Sep 20, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: be569f13-f15a-42a5-a53a-bfa929816f1f

📥 Commits

Reviewing files that changed from the base of the PR and between b137cc3 and 8e07f32.

📒 Files selected for processing (5)
  • app/api/styles/[slug]/rate/__tests__/route.test.ts
  • app/api/styles/[slug]/rate/route.ts
  • components/styles/style-rating.tsx
  • lib/supabase/migrations/041_style_ratings_user_binding.sql
  • lib/supabase/migrations/042_user_favorites_user_binding.sql

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

The PR adds user bindings for style ratings and favorites, backfills eligible legacy records, preserves legacy rating inserts, adds migration indexes and duplicate cleanup, and improves client handling of failed rating submissions.

Changes

User-bound ratings and favorites

Layer / File(s) Summary
Style ratings user binding
lib/supabase/migrations/041_style_ratings_user_binding.sql
Adds user_id to style ratings, backfills valid authenticated identities, removes duplicate user ratings, and creates partial indexes.
User favorites user binding
lib/supabase/migrations/042_user_favorites_user_binding.sql
Adds nullable user_id, makes session_id nullable, backfills valid identities, removes duplicate authenticated favorites, and creates indexes.
Rating insert compatibility
app/api/styles/[slug]/rate/route.ts, app/api/styles/[slug]/rate/__tests__/route.test.ts
Uses separate modern and legacy insert payloads. The test verifies that legacy inserts omit user_id.
Rating submission error handling
components/styles/style-rating.tsx
Safely parses failed responses, displays the server message or fallback text, resets the rating, and refreshes rating data.

Priority: ⬆️ High

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 8e07f

The rating and favorites schema changes preserve their respective write paths without an actionable current-head issue. The PR is mergeable after normal checks.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 3 files. (2 skipped: 2 … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary ratings fix: avoiding inserts that reference the missing production user_id column.
Description check ✅ Passed The description is detailed and covers the required summary, rationale, change type, validation, security, breaking changes, and reviewer notes. The selected Build / CI scope is slightly imprecise, bu…
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 3 files. (2 skipped: 2 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

❤️ Share

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

@AnxForever
AnxForever merged commit 97eb81e into main Sep 20, 2026
8 of 10 checks passed
@AnxForever
AnxForever deleted the fix/ugc-user-identity-binding branch September 20, 2026 07:44
Link-Start pushed a commit to Link-Start/stylekit_AnxForever that referenced this pull request Sep 21, 2026
stylekit-lint-example.yml sat in .github/workflows/, so it ran on every pull
request that touched components/**/*.tsx -- which is how it first fired, on
PR AnxForever#44. It could not pass there. It pointed at .github/actions/lint, whose two
candidate linter paths (packages/core/dist/linter/index.cjs and
lib/linter/index.ts) exist nowhere in this repository, and its glob
src/**/*.tsx matches nothing in a checkout that has no src/ directory.

Everything else about the file describes a consumer's repository: it installs
@stylekit/core from npm and lints src/**. So it moves to docs/examples/ as
documentation, and its uses: and input names now match the action the README
actually documents, .github/actions/stylekit-lint -- which takes fail-on-error,
not the fail-on that the workflow was passing to a different action entirely.
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