Give API-created projects an organization so they stay visible - #246
Merged
Conversation
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
ThreatCrush Security Scan39 finding(s) HIGH/CRITICAL: 2 | MEDIUM: 28 | LOW: 9
Snippets are redacted; ThreatCrush never prints matched credential material. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
A project with
organization_idNULL collects traffic perfectly and cannot be read. Bothapp/(app)/dashboard/page.tsxanddashboard/analytics/page.tsxscope their project query as: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.createSlotForSiteinserted projects with{owner_id, name, url, tracker_enabled}and noorganization_id, so every site born fromcrawlproof slots createwas invisible from birth, and thead_slotsrow 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
resolveProjectthen 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_slotsinsert below, which already readsproject.organization_id.@/lib/orgsis imported dynamically because it pulls inserver-only, which vitest cannot load, and this module's pure helpers (parseSlotRequest,embedFor,hostOf) are unit-tested intests/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:
999e9372-…and readm3.com528372c9-…attached to the Profullstack org, along with theirad_slotsrows.99d7f109-…(created by the GitHub install-tracker flow at 06:23 UTC on 2026-09-08) was deleted, after moving itsproject_reposandproject_pr_runsrows onto the real project so theprofullstack/niche-dbrepo link survived.?site=nichedb.devnow returns 188 pageviews,?site=readm3.comreturns 40. Zero org-less projects remain.Testing
tsc --noEmitexit 0vitest run— 2145 passed, 7 skipped, 166 filesbiome check lib/ads/slots.tsclean🤖 Generated with Claude Code
https://claude.ai/code/session_01NdTnhkYpjmTLAYrFH2f2yQ