# Pitfalls Research

**Domain:** Public-first discovery platform with catalog enrichment (GearBox v2.1)
**Researched:** 2026-04-09
**Confidence:** HIGH (based on direct codebase inspection of v2.0 + verified ecosystem patterns)

> v2.0 migration pitfalls (SQLite→Postgres, single→multi-user) are archived in git history.
> This document covers pitfalls specific to the v2.1 milestone: public access model, discovery feed, catalog enrichment, and agent-powered seeding.

---

## Critical Pitfalls

### Pitfall 1: Frontend Auth Guard Blocks All New Public Routes

**What goes wrong:**
The root layout (`__root.tsx`) hard-redirects any unauthenticated visitor to `/login` unless they are already on `/users/*` or `/login`. When public routes are added — a discovery landing page at `/`, a public catalog at `/global-items/` that is meant to be the new entry point — they will silently redirect anonymous users before rendering anything. The server already correctly skips auth middleware for `GET /api/global-items` (line 136 of `src/server/index.ts`), but the frontend guard is a separate allowlist that has not been updated.

**Why it happens:**
The client-side guard and the server-side middleware allowlist live in different files (`__root.tsx` vs `server/index.ts`) and can drift. Developers add routes to the server-side skip list but forget the frontend guard, then wonder why authenticated users see the feature but unauthenticated visitors hit the login page.

**How to avoid:**
Refactor the auth guard before building any public UI. Invert the logic: instead of allowlisting public routes, define a small `PROTECTED_ROUTES` set (collection, planning, settings, threads) and use TanStack Router's `beforeLoad` to protect those specific routes. Everything else renders without auth. The root layout should not gate render — it should only determine which UI chrome elements to show based on auth state.

**Warning signs:**
- Loading `/global-items/` in a private browser window redirects to `/login`
- The `isPublicRoute` check in `__root.tsx` is a string allowlist that grows as features are added
- New routes work for authenticated users but are invisible to anonymous users during testing

**Phase to address:**
Public access auth model phase — must be the first change made. Every other public feature depends on this being correct.

---

### Pitfall 2: `useAuth()` Spinner Blocks Public Page First Contentful Paint

**What goes wrong:**
The root layout shows a full-screen spinner while `useAuth()` resolves. For authenticated users this is imperceptible (~50ms for a cached session). For anonymous visitors on a public discovery page, this is 300–800ms of blank white screen before any content appears — because the auth check hits `/api/auth/me` which must complete before the page renders. This directly undercuts "public-first" positioning.

Additionally, `useOnboardingComplete()` fires for all users. For anonymous visitors it will hit an auth-required endpoint and produce a 401. Even though it is conditionally rendered, verify the hook itself does not fetch when `isAuthenticated` is false.

**Why it happens:**
Login-first apps legitimately gate the entire UI on auth resolution — there is nothing useful to show an unauthenticated user. The same pattern applied to a public discovery page creates a perceived login wall.

**How to avoid:**
Public routes must render immediately with unauthenticated defaults. Auth state loads in the background and hydrates progressive elements (nav user avatar, "Add to collection" CTAs) without blocking content. Use React Query's `enabled: isAuthenticated` on all hooks that call auth-required endpoints. The `useAuth()` query itself should never block page render — only auth-gated actions should wait on it.

**Warning signs:**
- Full-screen spinner visible to anonymous visitors on the landing page
- Lighthouse FCP score degrades after the public access change
- Network tab shows 401 on `/api/settings` or `/api/totals` for logged-out users

**Phase to address:**
Public access auth model phase — same as Pitfall 1, tackled together.

---

### Pitfall 3: Root-Level Components Fire Auth-Required Queries for Anonymous Users

**What goes wrong:**
`TotalsBar` is rendered at the root layout level for all routes and calls `useTotals()` which hits `GET /api/totals`. The auth middleware does not skip `/api/totals` for GET requests (verified in `server/index.ts`) — it requires a `userId`. Anonymous visitors will receive a 401 on every public page load, and React Query will retry the failed query three times. `FabMenu`, `CatalogSearchOverlay`, `AddToCollectionModal`, and `AddToThreadModal` are similarly rendered at root level and may trigger auth-gated operations.

**Why it happens:**
Root layout components were designed when every user was authenticated. Adding public routes does not automatically suppress these components' data fetches.

**How to avoid:**
Audit every component rendered in the root layout. For each one: (1) does it make an API call? (2) does that endpoint require auth? If yes, add `enabled: isAuthenticated` to the query, or conditionally render the component itself behind `{isAuthenticated && <TotalsBar />}`. `TotalsBar` should not appear on the new public discovery landing page at all — it is a user-specific widget.

**Warning signs:**
- Network tab shows 401 on `/api/totals` for anonymous users
- React Query error boundaries firing on public pages for components that are not relevant to anonymous users
- Console shows `[auth] OIDC auth failed` log spam from root-level queries

**Phase to address:**
Public access auth model phase — audit and guard every root-level component before deploying the public landing page.

---

### Pitfall 4: Discovery Feed Built as Per-Card Queries (N+1)

**What goes wrong:**
A discovery feed showing popular public setups or recently added catalog items typically starts as a list query followed by per-item detail fetches. For example: `getAllPublicSetups()` returns 20 setup IDs, then the frontend or backend fetches each setup's item count, owner display name, and total weight individually. At 20 items this is invisible; at 100+ items or with multiple feed sections it causes 2+ second response times and high DB connection pressure.

The existing `getPublicSetupWithItems()` service function is optimized for a single-setup detail view. Reusing it in a loop for a feed is the most common trap.

**Why it happens:**
Developers reach for familiar service functions. The function works. Performance issues only appear under real data volumes, not in development with 3 test setups.

**How to avoid:**
Write dedicated feed query functions using Drizzle joins from day one. A single SQL query should return all feed cards with their aggregates (item count, total weight in grams, owner display name). Add PostgreSQL indexes on `setups.is_public`, `setups.created_at`, and `setups.updated_at` before building the feed query. Mirror the pattern already used for aggregate totals (computed via SQL on read, not stored).

**Warning signs:**
- Feed query time scales linearly with results count
- `pg_stat_statements` shows repeated single-row lookups for users or items
- Adding a second feed section doubles total response time

**Phase to address:**
Discovery landing page phase — design feed queries as joins from the first implementation, not as a later optimization.

---

### Pitfall 5: Image Attribution Stored as Unstructured Text

**What goes wrong:**
If image attribution for catalog items is stored as a single `attribution: text` field (the fastest approach), it becomes impossible to: programmatically render a copyright badge, distinguish manufacturer press images from community uploads from AI-generated placeholders, enforce a "no scraped retailer images" policy, or filter catalog items by image source type. Agent-seeded catalog items will have inconsistent attribution formats that are expensive to clean up retroactively.

The current `globalItems` schema has only `imageUrl: text`. There is no `imageSourceType` or structured attribution.

**Why it happens:**
"We'll add a text note" is the zero-friction path. Attribution structure seems like a nice-to-have until you need to answer "how many catalog items have manufacturer-licensed images?" or build a compliance filter.

**How to avoid:**
Define a structured attribution model at schema design time before any seeding. Minimum: `imageSourceType: text` (enum: `manufacturer`, `community`, `agent_seeded`, `no_image`), `imageAttribution: text` (human-readable credit line), and `imageSourceUrl: text` (already exists on items but not on globalItems). This allows source-type-specific rendering and filtering without a schema migration mid-catalog-build.

**Warning signs:**
- Seeding agent instructions say "put attribution in the description field"
- Catalog items display images without any credit indication
- No way to query "show me only manufacturer-sourced images"

**Phase to address:**
Catalog enrichment infrastructure phase — schema changes must be in the migration before any seeding begins.

---

### Pitfall 6: Agent Catalog Seeding Creates Duplicate Global Items

**What goes wrong:**
Without a unique constraint on `(brand, model)` in the `globalItems` table (which currently has none), running an MCP agent seeding pass twice creates duplicate rows for the same product. Agents also retry on API errors, compounding the issue. The current `create_item` MCP tool creates a new row unconditionally — it was designed for personal collection management where duplicates are intentional (a user can own two of the same item). Reusing it for catalog seeding carries no deduplication.

**Why it happens:**
The catalog seeding flow is built on top of existing personal item tools because they are already available via MCP. The semantic mismatch (user-owned vs. global reference item) is not obvious until duplicates appear.

**How to avoid:**
Add a unique constraint on `globalItems(brand, model)` as part of the catalog enrichment schema migration. Create a dedicated `upsert_catalog_item` MCP tool or admin API endpoint that uses `ON CONFLICT (brand, model) DO UPDATE` semantics. This tool should be explicitly different from personal collection tools: no `userId`, upsert behavior, admin-scoped access.

**Warning signs:**
- Catalog search returns two entries for the same product ("Apidura Backcountry Food Pouch")
- Owner count on a duplicate item is 0 because user-owned items link to the wrong copy
- Re-running a seed script doubles the catalog size

**Phase to address:**
Catalog enrichment infrastructure phase — unique constraint and upsert endpoint before any agent seeding run.

---

### Pitfall 7: Storing Third-Party Product Images in S3 Creates Legal and Cost Exposure

**What goes wrong:**
The existing `upload_image_from_url` MCP tool fetches a URL and saves it to MinIO/S3. If an agent uses this to seed manufacturer product images from brand websites, retailer pages, or Amazon listings, those images are copyright-protected. Storing and publicly serving them creates: (1) legal liability for hosting images without a license — up to $150,000 per infringement in the US; (2) storage and egress costs that grow with public traffic; (3) dependency on external URLs that 404 silently when retailers change their CDN paths.

**Why it happens:**
"Just grab the product image from the brand website" produces accurate images immediately. It feels like fair use. It is not — attribution does not create a license, and copyright does not require a watermark or notice.

**How to avoid:**
Define a clear image sourcing policy before seeding begins. Safest options in order: (1) store `imageUrl` as a reference to the external source without copying to S3; (2) use manufacturer-provided press/media kit images that explicitly grant redistribution; (3) use Creative Commons–licensed images from Wikimedia Commons or similar. Document which sources are permitted in the seeding agent's prompt. Do not hotlink to third-party URLs either — they create external dependencies. Distinguish permitted images from unverified ones using `imageSourceType`.

**Warning signs:**
- Seeding instructions tell the agent to call `upload_image_from_url` on Amazon product listing URLs
- All catalog items have `imageFilename` values from manufacturer/retailer URLs
- No documented image licensing policy before seeding starts

**Phase to address:**
Catalog enrichment infrastructure phase — establish policy and `imageSourceType` schema before any seeding.

---

### Pitfall 8: MCP Catalog Tools Share the Seeding Agent's Personal userId

**What goes wrong:**
The MCP server binds every tool invocation to the `userId` of the authenticated API key or OAuth token. When an agent uses a regular user API key to create catalog items, those items are implicitly associated with that user's account context. This creates two problems: (1) catalog items appear in the seeding user's personal collection or produce permission collisions; (2) running the seeding agent as a specific user creates a "ghost user" with thousands of catalog entries that pollutes collection analytics and owner counts.

**Why it happens:**
There is no separation between personal collection MCP tools and catalog admin tools in the current implementation. The `userId` context flows through all tool handlers automatically.

**How to avoid:**
Catalog write operations must not carry a personal `userId`. Options: (1) create a separate admin-scoped API key that maps to a "system" user with no personal collection; (2) build dedicated catalog MCP tools that explicitly ignore `userId` for the globalItems table while still requiring authentication for authorization; (3) use a separate REST endpoint (`POST /api/admin/catalog-items`) with admin-only auth, bypassing the user-scoped MCP tools entirely.

**Warning signs:**
- Running the seeding agent creates items visible in someone's personal collection
- Owner count on seeded global items starts at 1 (the seeding user's implicit ownership)
- Catalog items appear in the seeding user's dashboard totals

**Phase to address:**
Catalog enrichment infrastructure phase — design catalog write path before building seeding tooling.

---

## Technical Debt Patterns

| Shortcut | Immediate Benefit | Long-term Cost | When Acceptable |
|----------|-------------------|----------------|-----------------|
| Single `isPublicRoute` allowlist in `__root.tsx` | Simple to reason about | Every new public route requires updating this list; lists drift | Never — use per-route `beforeLoad` guards on protected routes instead |
| Reuse personal item MCP tools for catalog seeding | No new tools to build | Creates wrong userId semantics, no deduplication, wrong ownership | Never for bulk ops — build a dedicated catalog upsert tool |
| `attribution: text` free-form field for image credit | Zero schema change | Cannot programmatically distinguish source types, filter, or enforce licensing policy | Only for internal admin-only catalog; never for public content |
| Hotlink external product images without copying to S3 | Zero storage cost | Silent 404s when retailers change CDN URLs; external dependency | Only for dev/prototype with a clear plan to replace |
| Discovery feed as multiple React Query calls per card | Familiar pattern | N+1 queries degrade at scale; visible at ~30 feed cards | Only for MVP with < 20 items and a committed optimization plan |
| No unique constraint on `globalItems(brand, model)` | Faster initial schema | Duplicate catalog entries after every re-seed or agent retry | Never — add the constraint before any seeding |

---

## Integration Gotchas

| Integration | Common Mistake | Correct Approach |
|-------------|----------------|------------------|
| Logto OIDC + public routes | `oidcAuthMiddleware()` throws or redirects when there is no session, breaking public routes | Use `getAuth(c)` which returns null gracefully for unauthenticated requests; only apply `oidcAuthMiddleware()` on login-gated routes |
| MCP tools + catalog seeding | Using user-scoped tools (bound to API key owner's `userId`) to write global catalog entries | Build separate catalog admin tools or a REST endpoint that writes to `globalItems` without personal userId semantics |
| MinIO/S3 + public catalog | Using presigned URLs (which expire) for catalog image delivery | Catalog item images need stable public paths or a CDN URL; presigned URLs are for user-private content only |
| TanStack Router `beforeLoad` + auth check | `beforeLoad` that re-fetches auth on every navigation creates a waterfall | Read from React Query cache (already has 5-min `staleTime` in `useAuth`); `beforeLoad` should read cached auth state, not re-fetch |
| PostgreSQL + public feed queries | Missing indexes on `is_public`, `created_at` cause full-table scans | Add composite indexes on `(is_public, created_at)` on setups table before the feed goes live |

---

## Performance Traps

| Trap | Symptoms | Prevention | When It Breaks |
|------|----------|------------|----------------|
| Per-card queries in discovery feed | Feed loads in > 2s; each section multiplies DB time | Single JOIN query returning all feed card data with aggregates | At ~30 items in feed |
| Auth check blocking public FCP | Blank + spinner visible on first load; LCP degraded | Render public content immediately; auth state hydrates progressively | Immediately on first deploy — visible in Lighthouse |
| Full-table scan on `globalItems` text search | Search feels fine at 18 items; slows visibly at 500+ | Add `pg_trgm` trigram index or `tsvector` GIN index before catalog grows | At ~200 catalog items |
| Image egress costs without CDN | MinIO egress scales with public traffic | CDN in front of public catalog images, or store external `imageUrl` references | Once catalog is publicly discoverable |
| React Query refetching public feed on every window focus | Unnecessary server load for anonymous browsing | Set appropriate `staleTime` (5–10 min) on public catalog/feed queries | At moderate traffic |

---

## Security Mistakes

| Mistake | Risk | Prevention |
|---------|------|------------|
| Regular user API key authorized to write global catalog items | Any user with an API key can pollute the shared catalog | Catalog write operations require admin scope or a designated system API key; regular user keys are read-only on globalItems |
| Public setup pages exposing private item fields | Public setup view leaks item notes, threads, or product URLs not intended for sharing | Audit `getPublicSetupWithItems` — return only explicitly public fields (name, weight, image); strip notes and thread data |
| No rate limiting on public catalog search endpoint | `GET /api/global-items?q=...` is unauthenticated; bots can enumerate or abuse it | Add basic rate limiting middleware to unauthenticated GET endpoints before making them discoverable |
| `imageSourceUrl` storing retailer order URLs with auth tokens in query params | Private session or order data in stored URLs | Normalize and validate `imageSourceUrl` before storage; strip query params that resemble auth or session tokens |

---

## UX Pitfalls

| Pitfall | User Impact | Better Approach |
|---------|-------------|-----------------|
| Hard login wall immediately after discovery | Anonymous users discover value, click a setup, hit a login wall — they leave | Show full public setup/item detail to anonymous users; only prompt login at the point of a write action (add to collection) |
| Empty state on catalog search with no query | Users expect to browse; zero results on open page is confusing | Return a curated/ranked set for empty queries (popular, recently added, or featured tags) |
| Catalog feed with no images | Text-only cards look sparse and unfinished | Ensure most catalog items have images before the feed is public; add a styled placeholder with brand initial |
| Replacing dashboard for logged-in users | Existing users lose their familiar personal dashboard entry point | Discovery page is the anonymous entry point; authenticated users see a hybrid or a personal dashboard — do not remove the existing dashboard |
| Agent-seeded content displayed raw without quality review | Inconsistent formatting, wrong weights, or invalid product links visible publicly | Implement `status: draft | published` on catalog items; agents create drafts, a review step publishes them |

---

## "Looks Done But Isn't" Checklist

- [ ] **Public route guard:** Routes `/`, `/global-items/`, `/global-items/:id`, and `/users/:id` render without redirect in a private browser window with no session cookies — verify manually before shipping
- [ ] **Root-level component suppression:** No 401 responses in the network tab when browsing public pages as an anonymous user — `TotalsBar`, `FabMenu`, and `OnboardingWizard` must not fire auth-required queries
- [ ] **Catalog deduplication:** Running the agent seed script twice does not increase the row count in `globalItems` — verify unique constraint exists and upsert behavior works
- [ ] **Image attribution schema:** `globalItems` has `imageSourceType` column in the migration before any seeding starts — verify migration file exists and was applied
- [ ] **Feed query efficiency:** Discovery feed data loads from a single JOIN query — verify using `EXPLAIN ANALYZE` or query logging, not by eyeballing response time
- [ ] **Public setup privacy:** `getPublicSetupWithItems` response does not include item `notes`, thread data, or private product URLs — verify the response shape manually
- [ ] **Catalog write authorization:** A regular user's API key cannot create or modify `globalItems` — verify the catalog tool/endpoint requires admin scope
- [ ] **Image copyright policy:** Seeding instructions explicitly specify which image sources are permitted; no `upload_image_from_url` calls against brand/retailer URLs — verify in the agent prompt before any seeding run

---

## Recovery Strategies

| Pitfall | Recovery Cost | Recovery Steps |
|---------|---------------|----------------|
| Login redirect blocking public routes | LOW | Update `isPublicRoute` allowlist in `__root.tsx` and add server-side guard bypasses; redeploy; verify in incognito |
| Duplicate catalog items from agent seeding | MEDIUM | Write a deduplication migration to merge duplicates keeping owner links; add unique constraint post-merge; re-run seed in upsert mode |
| Copyrighted images stored in S3 | HIGH | Identify affected items via `imageSourceType`; delete S3 objects; replace with permitted images or null `imageFilename`; legal review |
| N+1 feed queries causing degraded response times | MEDIUM | Write optimized JOIN query; API response shape may change requiring frontend update; deploy together |
| Auth-scoped queries firing for anonymous users | LOW | Add `enabled: isAuthenticated` to each affected query; guard root-level components with auth check |
| Catalog items created with seeding user's userId | MEDIUM | Migration to null out `userId` on globalItems created during seeding; update catalog write path to not accept userId |

---

## Pitfall-to-Phase Mapping

| Pitfall | Prevention Phase | Verification |
|---------|------------------|--------------|
| Frontend auth guard blocks public routes (P1) | Public access auth model | Load `/global-items/` and `/` in private window — no redirect |
| `useAuth()` spinner blocks public FCP (P2) | Public access auth model | Lighthouse FCP on landing page with cold cache — no full-screen spinner |
| Root-level components 401 for anonymous users (P3) | Public access auth model | Zero 401 responses in network tab on public pages |
| Discovery feed N+1 queries (P4) | Discovery landing page | `EXPLAIN ANALYZE` on feed endpoint confirms single query, no per-row loops |
| Image attribution stored as free text (P5) | Catalog enrichment infrastructure | Schema review — `imageSourceType` column exists on `globalItems` before seeding |
| Agent seeding creates duplicates (P6) | Catalog enrichment infrastructure | Run seed script twice — row count unchanged on second run |
| Copyrighted images in S3 (P7) | Catalog enrichment infrastructure | Seeding instructions reviewed — no calls to `upload_image_from_url` on brand URLs |
| Agent catalog tools carry personal userId (P8) | Catalog enrichment infrastructure | Seeded items have null userId or system userId; not in any user's collection |

---

## Sources

- GearBox codebase: `src/client/routes/__root.tsx` — root auth guard and `isPublicRoute` allowlist (direct inspection)
- GearBox codebase: `src/server/index.ts` — server-side public route bypass patterns (direct inspection)
- GearBox codebase: `src/db/schema.ts` — `globalItems` table confirming no unique constraint on brand/model, no `imageSourceType` (direct inspection)
- GearBox codebase: `src/server/mcp/index.ts` — MCP userId binding per API key (direct inspection)
- [TanStack Router: Auth performance issue with recommended patterns (GitHub #3997)](https://github.com/TanStack/router/issues/3997)
- [TanStack Router: Authenticated Routes documentation](https://tanstack.com/router/v1/docs/guide/authenticated-routes)
- [Practical Ecommerce: Online Retailer's Guide to Photo Copyrights](https://www.practicalecommerce.com/Online-Retailers-Guide-to-Photo-Copyrights)
- [MCP Idempotency: Best Practices 2025 (BytePlus)](https://www.byteplus.com/en/topic/542207)
- [Six Fatal Flaws of MCP (Scalifiai, 2025)](https://www.scalifiai.com/blog/model-context-protocol-flaws-2025)
- [Hostwinds: Hotlinking Pitfalls and How to Protect Yourself](https://www.hostwinds.com/blog/hotlinking-pitfalls-and-how-to-protect-yourself)

---
*Pitfalls research for: GearBox v2.1 — Public-first discovery platform with catalog enrichment*
*Researched: 2026-04-09*
