Skip to content

Give API-created projects an organization so they stay visible - #246

Merged
ralyodio merged 1 commit into
masterfrom
worktree-slots-org-id
Sep 8, 2026
Merged

Give API-created projects an organization so they stay visible#246
ralyodio merged 1 commit into
masterfrom
worktree-slots-org-id

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

The bug

A project with organization_id NULL collects traffic perfectly and cannot be read. Both app/(app)/dashboard/page.tsx and dashboard/analytics/page.tsx scope their project query as:

.eq("organization_id", selectedOrg.id).or(accessFilter)

PostgREST ANDs those, so an org-less row is dropped for its own owner the moment an org is selected in the picker. Owner RLS (projects owner all) never fires — the app filter is the gate.

createSlotForSite inserted projects with {owner_id, name, url, tracker_enabled} and no organization_id, so every site born from crawlproof slots create was invisible from birth, and the ad_slots row it wrote was org-less too.

The failure is quiet in the worst way: the dashboard shows nothing, so the next "add site" creates a duplicate project for the same hostname, and resolveProject then matches the empty twin. ?site=<uuid> returned real numbers while ?site=<hostname> returned zeros.

Found on nichedb.dev, which had spent three days recording 199 pageviews that no page would show. readm3.com had the identical defect — they were the only 2 org-less projects out of 53, and both came from this code path.

The fix

Attach an org once the site is resolved, covering both the project just created and any older org-less row matched by hostname. Setting it on the local object also carries the org onto the ad_slots insert below, which already reads project.organization_id.

@/lib/orgs is imported dynamically because it pulls in server-only, which vitest cannot load, and this module's pure helpers (parseSlotRequest, embedFor, hostOf) are unit-tested in tests/ads-api-requests.test.ts. A missing org schema is caught and still yields a working slot.

Production data already repaired

Both affected projects were fixed directly, ahead of this PR:

  • nichedb.dev 999e9372-… and readm3.com 528372c9-… attached to the Profullstack org, along with their ad_slots rows.
  • The duplicate nichedb.dev project 99d7f109-… (created by the GitHub install-tracker flow at 06:23 UTC on 2026-09-08) was deleted, after moving its project_repos and project_pr_runs rows onto the real project so the profullstack/niche-db repo link survived.
  • Verified after: ?site=nichedb.dev now returns 188 pageviews, ?site=readm3.com returns 40. Zero org-less projects remain.

Testing

  • tsc --noEmit exit 0
  • vitest run — 2145 passed, 7 skipped, 166 files
  • biome check lib/ads/slots.ts clean

🤖 Generated with Claude Code

https://claude.ai/code/session_01NdTnhkYpjmTLAYrFH2f2yQ

A project with organization_id NULL collects traffic perfectly and cannot
be read. Both the portfolio and the analytics page scope their query with
.eq("organization_id", selectedOrg.id).or(accessFilter), and PostgREST
ANDs those, so the row is dropped for its own owner as soon as an org is
selected in the picker. Owner RLS never fires; the app filter is the gate.

createSlotForSite inserted projects with owner_id, name, url and
tracker_enabled only, so every site born from `crawlproof slots create`
was invisible from birth, and its ad_slots row was org-less too. The
failure is quiet in the worst way: the dashboard shows nothing, so the
next "add site" creates a duplicate project for the same hostname, and
resolveProject then matches the empty twin. nichedb.dev spent three days
recording 199 pageviews that no page would show, and readm3.com had the
same defect.

Attach an org once the site is resolved, covering both the project we
just created and any older org-less row we matched by hostname. Setting
it on the local object also carries the org onto the ad_slots insert
below, which reads project.organization_id.

@/lib/orgs is imported dynamically because it pulls in server-only,
which vitest cannot load, and this module's pure helpers (parseSlotRequest,
embedFor, hostOf) are unit-tested in tests/ads-api-requests.test.ts. A
missing org schema is caught and still yields a working slot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NdTnhkYpjmTLAYrFH2f2yQ
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

39 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 28 | LOW: 9

Severity Rule Location
HIGH tls-verification-disabled lib/onion.ts:48
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
MEDIUM js-unescaped-html-sink app/(app)/dashboard/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/dashboard/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:41
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:324
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:99
MEDIUM redos-nested-quantifier lib/tracker/agent-gate.ts:61
MEDIUM sh-remote-script-execution prober/deploy/provision.sh:30
MEDIUM sql-template-interpolation scripts/detect-slot-themes.ts:31
MEDIUM sql-template-interpolation scripts/purge-constructed-keywords.ts:163
MEDIUM sql-template-interpolation scripts/purge-offniche-keywords.ts:124
LOW secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
LOW secret-generic-credential lib/sp/platforms/linkedin.ts:25
LOW js-dynamic-code-execution tests/careers-page-templates.test.ts:21
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:19
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:69
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 3cc64b0 into master Sep 8, 2026
10 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