From 8f4d979793b1a260262002a3aa1a3522cf9235af Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 8 Sep 2026 14:29:22 +0000 Subject: [PATCH] Give API-created projects an organization so they stay visible 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) Claude-Session: https://claude.ai/code/session_01NdTnhkYpjmTLAYrFH2f2yQ --- lib/ads/slots.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/ads/slots.ts b/lib/ads/slots.ts index e6884da..784dbd3 100644 --- a/lib/ads/slots.ts +++ b/lib/ads/slots.ts @@ -140,6 +140,27 @@ export async function createSlotForSite(input: { } if (!project) return { ok: false, status: 500, error: "Failed to resolve the site." }; + // A project with no organization is invisible in the dashboard: both the + // portfolio and the analytics page scope their query with + // `.eq("organization_id", selectedOrg.id).or(accessFilter)`, and PostgREST + // ANDs those, so an org-less row is dropped for its own owner the moment an + // org is picked. It still collects traffic perfectly — it just cannot be + // read — and the next "add site" mints a duplicate that shadows it in every + // lookup by hostname. Attach one on the way in, for the project we just + // created and for any older org-less row we found. + // + // Imported dynamically: `@/lib/orgs` pulls in `server-only`, which vitest + // cannot load, and this module's pure helpers are unit-tested. + if (!project.organization_id) { + try { + const { ensureProjectOrg } = await import("@/lib/orgs"); + const orgId = await ensureProjectOrg({ projectId: project.id, userId }); + if (orgId) project.organization_id = orgId; + } catch { + // An install without the org schema still gets a working slot. + } + } + const select = "id, status, placement, formats, project_id, created_at"; const { data: existing } = await sb .from("ad_slots")