Skip to content

OG image hardening, install rate-limit logging, and duplicate-company prevention#399

Closed
leerob wants to merge 2 commits into
mainfrom
cursor/og-image-hardening-and-install-rate-limit
Closed

OG image hardening, install rate-limit logging, and duplicate-company prevention#399
leerob wants to merge 2 commits into
mainfrom
cursor/og-image-hardening-and-install-rate-limit

Conversation

@leerob
Copy link
Copy Markdown
Collaborator

@leerob leerob commented May 26, 2026

Summary

This branch bundles three fixes:

Prevent duplicate company records

  • Companies could be created repeatedly (e.g. 7 separate "Contentful" rows) because the upsert only conflicted on a freshly-minted client-side id, while a BEFORE INSERT trigger silently uniquified slugs (contentful-1, contentful-2, …) instead of rejecting dupes.
  • Adds a normalized name_key generated column + a case-insensitive unique index (companies_name_key_unique) so duplicates are rejected at the DB level, even under concurrent/double submits (migration supabase/migrations/20260526_companies_unique_name.sql).
  • Rewrites upsert-company to treat a request as an edit only when the row already exists, and to reuse the existing company on a name conflict (idempotent) instead of creating a duplicate.
  • Generates the company form id once per mount so it stops changing on every render (which previously also broke the logo upload path).

OG image hardening

  • Fixes OG image 500s and makes the shared lib/og.tsx rendering more resilient across the various opengraph-image routes.

Install rate-limit logging

  • Stops install rate-limit hits from being logged as errors in track-install.

Test plan

  • Submitting the same company name twice (incl. rapid double-click) resolves to a single company instead of creating duplicates.
  • Editing an existing company still works and preserves its slug.
  • OG images render without 500s across home, company, user, and plugin routes.
  • Install rate-limit responses no longer surface as errors in logs.

Note: the company de-dup migration and a one-time cleanup of 260 pre-existing duplicate rows were already applied to the production database; the cleanup is reversible via the companies_dedup_backup_20260526 table.

Made with Cursor


Note

Medium Risk
Company insert/upsert behavior and a schema migration affect data integrity and concurrent creates; OG and install changes are lower risk but touch many routes and user-visible counts.

Overview
This PR hardens company creation, Open Graph images, and plugin install tracking in three parallel tracks.

Duplicate companies: A migration adds a stored name_key (lower(btrim(name))) and a unique index so names are unique case-insensitively at the database. upsert-company now treats edits only when the row exists (not merely when a client id is present), uses explicit update/insert instead of id-only upsert, trims names, maps unique violations to user-facing errors on edit, and on insert reuses the existing company by name_key so double-submit/retry is idempotent. The company form keeps a stable nanoid per mount so upload paths and submitted ids do not change every render.

OG images: Shared lib/og gains resolveOgImageUrl for absolute Satori URLs, Cache-Control on generated images, and revalidate = 86400 on OG routes. Profile/plugin OG templates use resolved logo/avatar URLs and add display: "flex" on text nodes for layout reliability.

Install rate limits: track-install returns { tracked, rateLimited } instead of throwing; the plugin detail page bumps install count only when tracked and shows a toast when rate-limited.

Reviewed by Cursor Bugbot for commit 6baf7c5. Bugbot is set up for automated code reviews on this repo. Configure here.

leerob and others added 2 commits May 26, 2026 12:02
OG image routes were 500ing (~5% of traffic) on relative logo URLs and
Satori layout constraints. Normalize image src to absolute URLs, add
explicit display:flex on multi-child nodes, and set Cache-Control plus a
1d revalidate so social crawlers hit cached PNGs.

track-install now returns a non-throwing rate-limit result instead of
throwing ActionError, so expected rate limiting no longer shows up as
error-level logs. The client only increments the count on success and
toasts on rate limit.

Co-authored-by: Cursor <cursoragent@cursor.com>
Companies could be created repeatedly (e.g. 7x "Contentful") because the
upsert only conflicted on a fresh client-side id and a BEFORE INSERT trigger
silently uniquified slugs. Enforce uniqueness instead:

- Add a normalized name_key column + case-insensitive unique index so
  duplicates are rejected at the DB level, even under concurrent submits.
- Rewrite upsert-company to edit by id only when the row exists, and reuse
  the existing company on a name conflict instead of creating a duplicate.
- Generate the company form id once per mount so it stops changing on every
  render.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 26, 2026

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

Project Deployment Actions Updated (UTC)
cursor-directory Ready Ready Preview, Comment May 26, 2026 5:20pm

Request Review

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Reuses another user's company
    • On insert unique violation, the handler now only reuses the existing company when owner_id matches the authenticated user; otherwise it throws the duplicate-name error.

Create PR

Or push these changes by commenting:

@cursor push d722d437e2
Preview (d722d437e2)
diff --git a/apps/cursor/src/actions/upsert-company.ts b/apps/cursor/src/actions/upsert-company.ts
--- a/apps/cursor/src/actions/upsert-company.ts
+++ b/apps/cursor/src/actions/upsert-company.ts
@@ -121,16 +121,16 @@
         if (error.code === UNIQUE_VIOLATION) {
           const { data: existing } = await supabase
             .from("companies")
-            .select("id, slug")
+            .select("id, slug, owner_id")
             .eq("name_key", nameKey)
             .maybeSingle();
 
-          if (existing) {
+          if (existing && existing.owner_id === userId) {
             if (shouldRedirect) {
               redirect(`/c/${existing.slug}`);
             }
 
-            return existing;
+            return { id: existing.id, slug: existing.slug };
           }
 
           throw new ActionError("A company with this name already exists.");

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 6baf7c5. Configure here.

redirect(`/c/${existing.slug}`);
}

return existing;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reuses another user's company

Medium Severity · Logic Bug

When a new company insert hits a name unique violation, the handler returns the existing row by name_key without checking owner_id. A user creating a name already owned by someone else can succeed and be redirected to that other company's profile instead of receiving “A company with this name already exists.”

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6baf7c5. Configure here.

@leerob
Copy link
Copy Markdown
Collaborator Author

leerob commented May 26, 2026

Superseded: the OG-image/rate-limit changes landed in main via #398, and the company de-duplication + picker work now lives in its own clean PR #400 (branched off latest main). Closing this stale draft.

@leerob leerob closed this May 26, 2026
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