docs: complete project research
This commit is contained in:
@@ -1,182 +1,203 @@
|
||||
# Project Research Summary
|
||||
|
||||
**Project:** GearBox v1.3 — Research & Decision Tools
|
||||
**Domain:** Gear management — candidate comparison, setup impact preview, drag-to-reorder ranking with pros/cons
|
||||
**Researched:** 2026-03-16
|
||||
**Project:** GearBox v2.0 Platform Foundation
|
||||
**Domain:** Single-user to multi-user gear management and discovery platform
|
||||
**Researched:** 2026-04-03
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Executive Summary
|
||||
|
||||
GearBox v1.3 adds three decision-support features to the existing thread detail page: side-by-side candidate comparison, setup impact preview (weight/cost delta), and drag-to-reorder candidate ranking with pros/cons annotation. All four research areas converge on the same conclusion — the existing stack is sufficient and no new dependencies are required. `framer-motion@12.37.0` (already installed) provides the `Reorder` component for drag-to-reorder, eliminating the need for `@dnd-kit` (which lacks React 19 support) or any other library. Two of the three features (comparison view and impact preview) require zero schema changes and can be built as pure client-side derived views using data already cached by `useThread()` and `useSetup()`.
|
||||
GearBox v2.0 is a structural migration, not a feature addition. The project transforms a proven single-user gear tracker (React 19 + Hono + Drizzle + SQLite + Bun) into a multi-user platform with a global item database, structured community reviews, and public setup sharing. The research is clear on the sequencing: database migration and multi-user data scoping must come first because every other feature depends on user-owned records and Postgres. Skipping or deferring either creates cascading rework across all downstream features.
|
||||
|
||||
The recommended build sequence is dependency-driven: schema migration first (adds `sort_order`, `pros`, `cons` to `thread_candidates`), then ranking UI (uses the new columns), then comparison view and impact preview in parallel (both are schema-independent client additions). This order eliminates the risk of mid-feature migrations and ensures the comparison table can display rank, pros, and cons from day one rather than being retrofitted. The entire milestone touches 3 new files and 10 modified files — a contained, low-blast-radius changeset.
|
||||
The recommended stack additions are conservative and well-documented: PostgreSQL 16 (required by the auth provider and for concurrent access), Bun's native S3 client against self-hosted MinIO (zero new dependencies), and an external OIDC auth provider replacing the current cookie-session system. The existing stack is validated and stays intact. The largest implementation risk is the SQLite-to-Postgres migration — it is a full schema rewrite, not an automated conversion, and it forces every service function to become async, cascading through 6 service files, 7 route files, and 19 MCP tools.
|
||||
|
||||
The primary risks are implementation-level rather than architectural. Three patterns require deliberate design before coding: (1) use `tempItems` local state alongside React Query for drag reorder to prevent the well-documented flicker bug, (2) use `sortOrder REAL` (fractional) instead of `INTEGER` to avoid bulk UPDATE writes on every drag, and (3) treat impact preview as an "add vs replace" decision — not just a pure addition — since users comparing gear are almost always replacing an existing item, not stacking one on top. All three are avoidable with upfront design; recovery cost is low but retrofitting is disruptive.
|
||||
**Open decision — auth provider:** STACK.md recommends Logto (TypeScript-native, purpose-built for app auth, React SDK with hooks, requires only Postgres, MIT-licensed). ARCHITECTURE.md recommends Authentik (Python-based, full OIDC/OAuth2, self-hosted, requires Postgres and Redis). Both are valid. Logto integrates at the React layer via `@logto/react` and validates JWTs on the Hono backend via `jose`. Authentik integrates at the server layer via `@hono/oidc-auth` middleware and handles all session state — no React SDK needed. This decision must be resolved before the auth phase begins and affects infrastructure dependencies, React integration complexity, and future capability for proxy-mode SSO. See the Gaps section for resolution criteria.
|
||||
|
||||
## Key Findings
|
||||
|
||||
### Recommended Stack
|
||||
|
||||
Zero new dependencies are needed for this milestone. The existing stack handles all three features: Tailwind CSS for the comparison table layout, `framer-motion`'s `Reorder` component for drag ordering, Drizzle ORM + Hono + Zod for the one new write endpoint (`PATCH /api/threads/:id/candidates/reorder`), and TanStack Query for the new `useReorderCandidates` mutation. All other React Query hooks (`useThread`, `useSetup`, `useSetups`) already exist and return the data needed for comparison and impact preview without modification.
|
||||
The existing stack (React 19, Hono, Drizzle ORM, TanStack Router/Query, Tailwind v4, Zustand, Zod, Bun) is unchanged and validated. The following are net-new additions for v2.0.
|
||||
|
||||
**Core technologies:**
|
||||
- `framer-motion@12.37.0` (Reorder component): drag-to-reorder — already installed, React 19 peerDeps confirmed in `bun.lock`, replaces any need for `@dnd-kit`
|
||||
- `drizzle-orm@0.45.1`: three new columns on `thread_candidates` (`sort_order REAL`, `pros TEXT`, `cons TEXT`) plus one new service function (`reorderCandidates`)
|
||||
- Tailwind CSS v4: comparison table layout with `overflow-x-auto`, `sticky left-0` for frozen label column, `min-w-[200px]` per candidate column
|
||||
- TanStack Query v5 + existing hooks: impact preview and comparison view derived entirely from cached `useThread` + `useSetup` data — no new API endpoints on read paths
|
||||
- Zod v4: extend `updateCandidateSchema` with `sortOrder: z.number().finite()`, `pros: z.string().max(500).optional()`, `cons: z.string().max(500).optional()`
|
||||
- **PostgreSQL 16:** Primary database — replaces SQLite for concurrent multi-user access; required by both auth provider candidates; enables full-text search for the global item catalog
|
||||
- **Drizzle ORM pg-core:** Existing ORM, different dialect — switch from `drizzle-orm/bun-sqlite` to `drizzle-orm/bun-sql` (or `postgres-js` as fallback); schema must be rewritten from scratch using `pgTable`, not migrated from `sqliteTable`
|
||||
- **Bun native S3Client + MinIO:** Zero-dependency image storage — replaces `./uploads/` local filesystem; Bun ships a native S3 client with documented MinIO compatibility; no `@aws-sdk/client-s3` needed
|
||||
- **jose (^6.2.2):** JWT validation — verifies OIDC access tokens on the Hono backend via JWKS; zero dependencies, explicit Bun support; needed regardless of auth provider choice
|
||||
- **@logto/react (^4.0.13) OR @hono/oidc-auth:** Auth provider SDK — one of these two depending on the provider decision (see open decision above)
|
||||
- **@electric-sql/pglite:** In-process Postgres for tests — replaces `bun:sqlite` in-memory test setup; Drizzle supports it via `drizzle-orm/pglite`; avoids Docker dependency in unit/integration tests
|
||||
- **postgres (postgres.js, ^3.4.8):** Dev dependency only — required for `drizzle-kit` CLI (`db:generate`, `db:push`) due to known issue #4122 where drizzle-kit does not support the Bun native SQL driver
|
||||
|
||||
**What NOT to use:**
|
||||
- `@dnd-kit/core@6.3.1` — no React 19 support, unmaintained for ~1 year
|
||||
- `@dnd-kit/react@0.3.2` — pre-1.0, no maintainer response on stability
|
||||
- `@hello-pangea/dnd@18.0.1` — `peerDep react: "^18.0.0"` only, stale
|
||||
- Any third-party comparison table component — custom Tailwind table is trivial and design-consistent
|
||||
**Infrastructure additions:** Docker Compose running Postgres + auth provider + MinIO alongside the Bun app. The app itself continues to run on bare Bun for HMR.
|
||||
|
||||
### Expected Features
|
||||
|
||||
All five v1.3 features are confirmed as P1 (must-have for this milestone). No existing gear management tool (LighterPack, GearGrams, OutPack) has comparison view, delta preview, or ranking — these are unmet-need differentiators adapted from e-commerce comparison UX to the gear domain.
|
||||
**Must have for v2.0 launch (P1):**
|
||||
- External auth provider integration — nothing works without multi-user identity
|
||||
- PostgreSQL migration — concurrent access and auth provider dependency
|
||||
- Multi-user data model (userId FK on items, categories, threads, setups, settings) — data isolation foundation
|
||||
- User profiles (minimal: display name, avatar, bio, public setups list) — required for attribution on shared content
|
||||
- Setup visibility controls (public/private toggle, default private) — table stakes for any sharing feature
|
||||
- Public setup detail pages — shareable read-only view with item list, totals, creator attribution
|
||||
- Global item database with seed data — canonical product catalog enabling reviews and aggregation
|
||||
- Link personal items to global items — the bridge enabling owner counts, crowd specs, and weight data
|
||||
- Search global items — full-text search powering item linking and discovery browsing
|
||||
- Structured reviews (overall + dimension ratings, no freeform text) — community intelligence layer
|
||||
- Item detail pages (aggregated specs, owner count, average ratings) — integration hub for all platform data
|
||||
- Discovery browse page (recent public setups, recently reviewed items, popular gear) — entry point for community value
|
||||
|
||||
**Must have (table stakes):**
|
||||
- Side-by-side comparison view — users juggling 3+ candidates mentally across cards expect tabular layout; NNGroup and Smashing Magazine confirm this is the standard for comparison contexts
|
||||
- Weight and cost delta per candidate — gear apps always display weight prominently; delta is more actionable than raw weight
|
||||
- Setup selector for impact preview — required to contextualize the delta; `useSetups()` already exists
|
||||
**Should have after validation (P2):**
|
||||
- Crowd-verified specs display (manufacturer vs. community-measured weight, needs 3+ owners per item)
|
||||
- Setup composition insights ("commonly paired with" co-occurrence analysis)
|
||||
- Planning thread global item integration (candidates auto-populate from global DB)
|
||||
- Copy/fork public setups (one-click template from public setups)
|
||||
- Popular gear rankings by category (most owned, highest rated)
|
||||
|
||||
**Should have (differentiators):**
|
||||
- Drag-to-rank ordering — makes priority explicit without numeric input; no competitor has this in the gear domain; requires `sort_order` schema migration
|
||||
- Per-candidate pros/cons fields — structured decision rationale; stored as newline-delimited text (renders as bullets in comparison view); requires `pros`/`cons` schema migration
|
||||
**Defer to v3+:**
|
||||
- Freeform reviews with moderation (explicitly deferred until moderation infrastructure exists)
|
||||
- Comments on setups (moderation burden)
|
||||
- Follow users / activity feed (social-network complexity, against the discovery-first principle)
|
||||
- OAuth / social login (after external auth is stable)
|
||||
|
||||
**Defer (v2+):**
|
||||
- Classification-aware impact breakdown (base/worn/consumable) — data available but UI complexity high; flat delta covers 90% of use case
|
||||
- Rank badge on card grid — useful but low urgency; add when users express confusion
|
||||
- Mobile-optimized comparison view (swipe between candidates) — horizontal scroll works for now
|
||||
- Comparison permalink — requires auth/multi-user work not in scope for v1
|
||||
|
||||
**Anti-features (explicitly rejected):**
|
||||
- Custom comparison attributes — complexity trap, rejected in PROJECT.md
|
||||
- Score/rating calculation — opaque algorithms distrust; manual ranking expresses user preference better
|
||||
- Cross-thread comparison — candidates are decision-scoped; different categories are not apples-to-apples
|
||||
**Anti-features to reject explicitly:** real-time collaborative setups, marketplace/buy-sell, AI gear recommendations, wiki-style open item editing, gamification, Instagram-style infinite scroll feed.
|
||||
|
||||
### Architecture Approach
|
||||
|
||||
All three features integrate on the `/threads/$threadId` route with no impact on other routes. The comparison view and impact preview are pure client-side derived views using data already in the React Query cache — no new API endpoints on read paths. The only new server-side endpoint is `PATCH /:id/candidates/reorder` which accepts `{ orderedIds: number[] }` and applies a transactional bulk-update in `thread.service.ts`. The `uiStore` (Zustand) gains two new fields: `compareMode: boolean` and `impactSetupId: number | null`, consistent with existing UI-state-only patterns.
|
||||
The v2.0 architecture is a layered structural migration where each layer depends on the one below it: Postgres first (database), then user identity (auth), then data scoping (userId on all entities), then the global item catalog, then community features on top. Every existing service file gains a `userId` parameter and becomes `async` — this is a mechanical but wide-ranging change touching 6 services, 7 routes, 19 MCP tools, and all tests. The component topology stays the same (Hono routes -> services -> Drizzle); only the wiring within each layer changes.
|
||||
|
||||
**Major components:**
|
||||
1. `CandidateCompare.tsx` (new) — side-by-side table; columns = candidates, rows = attributes; pure presentational, derives deltas from `thread.candidates[]`; `overflow-x-auto` for narrow viewports; sticky label column
|
||||
2. `SetupImpactRow.tsx` (new) — delta display (`+Xg / +$Y`); reads from `useSetup(impactSetupId)` data passed as props; handles null weight case explicitly
|
||||
3. `Reorder.Group` / `Reorder.Item` (framer-motion, no new file) — wraps `CandidateCard` list in `$threadId.tsx`; `onReorder` updates local `orderedCandidates` state; `onDragEnd` fires `useReorderCandidates` mutation
|
||||
4. `CandidateCard.tsx` (modified) — gains `rank` prop (gold/silver/bronze badge for top 3), pros/cons indicator icons; `isActive={false}` when rendered inside comparison view
|
||||
5. `CandidateForm.tsx` (modified) — gains `pros`/`cons` textarea fields below existing Notes field
|
||||
|
||||
**Key patterns to follow:**
|
||||
- `tempItems` local state alongside React Query for drag reorder — prevents the documented flicker bug; do not use `setQueryData` alone
|
||||
- Client-computed derived data from cached queries — no new read endpoints (anti-pattern: building `GET /api/threads/:id/compare` or `GET /api/threads/:id/impact`)
|
||||
- `uiStore` for cross-panel persistent UI flags only — no server data in Zustand
|
||||
- Resolved-thread guard — `thread.status === "resolved"` must disable drag handles and block the reorder endpoint (data integrity requirement, not just UX)
|
||||
1. **Database layer (Postgres + pg-core)** — Full schema rewrite; all entity tables gain userId FK; new `globalItems` and `reviews` tables; sessions table removed; `categories` unique constraint changes to composite `(userId, name)`
|
||||
2. **Auth middleware (OIDC)** — Replaces `requireAuth`; resolves OIDC subject to local userId via `getOrCreateUser`; keeps API key system intact for MCP and programmatic access; sessions table deleted (OIDC handles state via signed JWT cookies)
|
||||
3. **User-scoped services** — All existing services gain `userId` parameter and async signature; 4 new services added: `globalItem.service`, `review.service`, `profile.service`, `discover.service`
|
||||
4. **Image storage layer (MinIO via Bun S3Client)** — Replaces filesystem writes; abstraction interface allows local dev vs. S3 production swap; existing `/uploads/*` static route replaced by proxy or presigned URL handler
|
||||
5. **Global item catalog** — Separate `globalItems` table (not the user `items` table); admin-seeded initially; user items optionally reference global items via nullable `globalItemId` FK; reviews and owner counts attach to global items, not user items
|
||||
6. **Public content layer** — Setup `isPublic` flag; public profile pages; discovery queries over public content with indexes on `owner_count`, `(is_public, updated_at)`, and `global_item_id`
|
||||
|
||||
### Critical Pitfalls
|
||||
|
||||
1. **Drag flicker from `setQueryData`-only optimistic update** — use `tempItems` local state (`useState<Candidate[] | null>(null)`); render from `tempItems ?? queryData.candidates`; clear on mutation `onSettled`. Must be designed before building the drag UI, not retrofitted. (PITFALLS.md Pitfall 1)
|
||||
1. **Missing userId filters leak data between users** — Any service function not updated to filter by `userId` returns all users' data across 30+ query sites. Prevention: use `userId NOT NULL` in schema so TypeScript compiler errors guide updates; add Postgres Row-Level Security as a safety net; write cross-user isolation tests per entity (create as User A, query as User B, assert empty results).
|
||||
|
||||
2. **Integer `sortOrder` causes bulk writes** — use `REAL` (float) type for `sort_order` column with fractional indexing so only the moved item requires a single UPDATE. With 8+ candidates and rapid dragging, integer bulk updates produce visible latency and hold a SQLite write lock. Start values at 1000 with 1000-unit gaps. (PITFALLS.md Pitfall 2)
|
||||
2. **Drizzle schema rewrite is a replacement, not a migration** — `sqlite-core` and `pg-core` are incompatible; `real()` in Postgres is 4-byte float vs. SQLite's 8-byte (use `doublePrecision()` for weight values); timestamps change from integer epoch to native `timestamp`; all service functions must become async (`.get()` and `.all()` are sync SQLite methods, Postgres uses `await`). Prevention: rewrite schema from scratch, update all `.get()` / `.all()` calls, run full test suite against Postgres.
|
||||
|
||||
3. **Impact preview shows wrong delta (add vs replace)** — default to "replace" mode when a setup item exists in the same category as the thread; default to "add" mode when no category match. Pure-addition delta misleads users: a 500g candidate replacing an 800g item shows "+500g" instead of "-300g". The distinction must be designed into the service layer, not retrofitted. (PITFALLS.md Pitfall 6)
|
||||
3. **Test infrastructure collapses during DB switch** — `createTestDb()` uses `bun:sqlite` in-memory SQLite. After the switch, every test needs Postgres. Prevention: adopt PGlite (`@electric-sql/pglite`) for unit/integration tests immediately; never let the SQLite test setup coexist with Postgres production code past the migration sprint.
|
||||
|
||||
4. **Comparison/rank on resolved threads** — `thread.status === "resolved"` must hide drag handles, disable rank mutation, and show a read-only summary. The reorder API route must return 400 for resolved threads. This is a data integrity issue, not just UX. (PITFALLS.md Pitfall 8)
|
||||
4. **Auth migration breaks sessions, API keys, and MCP** — Switching to external OIDC touches the login flow, session management, API key ownership, MCP authentication, E2E test setup, and the onboarding flow. Prevention: keep API keys in the local database (do not delegate to the auth provider); maintain a local `users` table with `externalId` (OIDC subject) FK; keep `apiKeys` table with `userId` FK to local users; update E2E tests to authenticate via API keys.
|
||||
|
||||
5. **Test helper schema drift** — every schema change must update `tests/helpers/db.ts` in the same commit. Run `bun test` immediately after schema + helper update. Missing this produces `SqliteError: no such column` failures. (PITFALLS.md Pitfall 7)
|
||||
5. **Global item database creates a data model fork if built wrong** — An `isGlobal` flag or `NULL userId` on the user `items` table makes queries unmaintainable and blurs permission boundaries. Prevention: separate `globalItems` table from day one; user items get a nullable `globalItemId` FK; reviews and owner counts attach to `globalItems` only.
|
||||
|
||||
6. **Existing data has no owner after migration** — Current SQLite data has no `userId`. Adding `userId NOT NULL` breaks migration if existing rows are not assigned an owner first. Prevention: data migration script must create the original user first, assign all existing data to that userId, then enforce NOT NULL — never make `userId` permanently nullable as a migration workaround.
|
||||
|
||||
## Implications for Roadmap
|
||||
|
||||
Based on research, a 4-phase structure is recommended with a clear dependency order: schema foundation first, ranking second (consumes new columns), then comparison view and impact preview as sequential client-only phases.
|
||||
The dependency chain is strict: Postgres -> Auth -> Multi-user scoping -> Global items -> Community features. Attempting to parallelize across this chain creates rework. Suggested phase structure:
|
||||
|
||||
### Phase 1: Schema Foundation + Pros/Cons Fields
|
||||
### Phase 1: Database Migration (SQLite to PostgreSQL)
|
||||
**Rationale:** Everything else depends on Postgres. Auth providers require it. Concurrent access requires it. Full-text search for the global item catalog requires it. Must be done first and tested completely before any feature work begins.
|
||||
**Delivers:** Postgres running locally and in CI; schema rewritten in pg-core; all service functions async; PGlite test infrastructure replacing `bun:sqlite`; one-time data migration script for existing SQLite data; drizzle.config.ts updated; all PRAGMA statements removed
|
||||
**Addresses:** Postgres migration (FEATURES.md P1)
|
||||
**Avoids:** Pitfall 3 (schema rewrite), Pitfall 4 (test infrastructure), Pitfall 10 (SQLite-specific patterns), Pitfall 12 (existing data ownership)
|
||||
**Research flag:** Well-documented migration pattern. STACK.md and ARCHITECTURE.md agree on the approach. No additional research needed — pitfalls are comprehensively documented with GearBox-specific code references.
|
||||
|
||||
**Rationale:** All ranking and pros/cons work shares a schema migration. Batching `sort_order`, `pros`, and `cons` into a single migration avoids multiple ALTER TABLE runs and ensures the test helper is updated once. Pros/cons field UI is low-complexity (two textareas in `CandidateForm`) and can be delivered immediately after the migration, making candidates richer before ranking is built.
|
||||
**Delivers:** `sort_order REAL NOT NULL DEFAULT 0`, `pros TEXT`, `cons TEXT` on `thread_candidates`; pros/cons visible in candidate edit panel; `CandidateCard` shows pros/cons indicator icons; `tests/helpers/db.ts` updated; Zod schemas extended with 500-char length caps
|
||||
**Addresses:** Side-by-side comparison row data (pros/cons), drag-to-rank prerequisite (sort_order)
|
||||
**Avoids:** Test helper schema drift (Pitfall 7), pros/cons as unstructured blobs (Pitfall 5 — newline-delimited format chosen at schema time)
|
||||
### Phase 2: Authentication Provider Integration
|
||||
**Rationale:** User identity must exist before userId can be added to any table. This phase also resolves the open Logto vs. Authentik decision.
|
||||
**Delivers:** External auth provider running in Docker; OIDC middleware on Hono; local `users` table with `externalId` (OIDC subject); API key system preserved and userId-scoped; E2E tests updated to use API key authentication; onboarding flow replaced with auth provider registration; MCP auth updated
|
||||
**Uses:** `jose` (JWT validation), `@logto/react` or `@hono/oidc-auth` depending on provider decision, Docker Compose
|
||||
**Avoids:** Pitfall 5 (auth breaks sessions/keys/MCP); integration gotcha (keep local users table with externalId, do not remove it)
|
||||
**Research flag:** NEEDS RESOLUTION before this phase can be planned — the Logto vs. Authentik decision. See Gaps section for resolution criteria. Once the provider is chosen, integration patterns are well-documented in official docs.
|
||||
|
||||
### Phase 2: Drag-to-Reorder Candidate Ranking
|
||||
### Phase 3: Multi-User Data Model
|
||||
**Rationale:** With user identity established, all entity tables can be scoped. This is the highest-risk phase because it touches every query in the codebase and is where data leaks occur if anything is missed.
|
||||
**Delivers:** `userId` NOT NULL on items, categories, threads, setups, settings, apiKeys; composite unique on `(userId, name)` for categories; `isPublic` boolean on setups; `resolveThread` propagates userId to newly created items; all service functions filter by userId; cross-user isolation tests passing per entity; Postgres RLS policies active; MCP tools user-scoped; settings migrated to per-user
|
||||
**Addresses:** Multi-user data model, setup visibility controls, user profile data model (FEATURES.md P1)
|
||||
**Avoids:** Pitfall 1 (missing userId filters), Pitfall 2 (category uniqueness), Pitfall 8 (thread resolution userId), Pitfall 9 (public content defaults to private), Pitfall 11 (setup sync race conditions)
|
||||
**Research flag:** No additional research needed — pitfall documentation is comprehensive with specific per-table and per-function guidance for the existing GearBox codebase.
|
||||
|
||||
**Rationale:** Depends on Phase 1 (`sort_order` column must exist). Schema work is done; this phase is pure service + client. The `tempItems` pattern must be implemented correctly from the start to prevent the React Query flicker bug.
|
||||
**Delivers:** `reorderCandidates` service function (transactional loop); `PATCH /api/threads/:id/candidates/reorder` endpoint with thread ownership validation; `useReorderCandidates` mutation hook; `Reorder.Group` / `Reorder.Item` in thread detail route; rank badge (gold/silver/bronze) on `CandidateCard`; resolved-thread guard (no drag handles, API returns 400 for resolved)
|
||||
**Uses:** `framer-motion@12.37.0` Reorder API (already installed), Drizzle ORM transaction, fractional `sort_order REAL` arithmetic (single UPDATE per drag)
|
||||
**Avoids:** dnd-kit flicker (Pitfall 1 — `tempItems` pattern), bulk integer writes (Pitfall 2 — REAL type), resolved-thread corruption (Pitfall 8)
|
||||
### Phase 4: Image Storage Migration (MinIO)
|
||||
**Rationale:** Move image storage before public profiles and discovery ship. Once images are served to unauthenticated users at scale, the local filesystem approach fails. Better to resolve this before public content launches.
|
||||
**Delivers:** MinIO running in Docker; Bun native S3Client configured; existing `./uploads/` migrated to MinIO bucket; image service updated; proxy route for S3 reads; MCP `upload_image_from_url` tool updated; image storage abstraction (local filesystem for dev, S3 for production)
|
||||
**Uses:** Bun native S3Client (built-in, no install), MinIO Docker container
|
||||
**Avoids:** Pitfall 7 (image URL breakage after storage migration)
|
||||
**Research flag:** Standard pattern. Bun S3Client docs and MinIO compatibility are well-documented. No research needed.
|
||||
|
||||
### Phase 3: Side-by-Side Comparison View
|
||||
### Phase 5: Global Item Database
|
||||
**Rationale:** The global item catalog is the second major foundation. Reviews, item detail pages, owner counts, and discovery all depend on canonical product records existing before those features can be built.
|
||||
**Delivers:** `globalItems` table in pg-core; admin seeding workflow with 200-500 initial items across core categories; nullable `globalItemId` FK on user items; item linking flow in collection UI; full-text search via Postgres `tsvector`; `GET /api/global-items` endpoints (public, no auth); `globalItem.service.ts`
|
||||
**Addresses:** Global item database, link personal items to global items, search global items (FEATURES.md P1)
|
||||
**Avoids:** Pitfall 6 (data model fork — separate `globalItems` table, not a flag on user items)
|
||||
**Research flag:** May benefit from targeted research on Postgres full-text search (`tsvector`/`tsquery`) configuration — specifically index design and query tuning for the expected catalog size and query patterns (brand + model name search). Schema is specified; FTS tuning is domain-dependent.
|
||||
|
||||
**Rationale:** No schema dependency — can technically be built before Phase 2, but is most useful when rank, pros, and cons are already in the data model so the comparison table shows the full picture from day one. Pure client-side presentational component; no API changes.
|
||||
**Delivers:** `CandidateCompare.tsx` component; "Compare" toggle button in thread header; `compareMode` in `uiStore`; comparison table with sticky label column, horizontal scroll, weight/price relative deltas (lightest/cheapest candidate highlighted); responsive at 768px viewport; read-only summary for resolved threads
|
||||
**Implements:** Client-computed derived data pattern — data from `useThread()` cache; `Math.min` across candidates for relative delta; `formatWeight`/`formatPrice` for display
|
||||
**Avoids:** Comparison breaking at narrow widths (Pitfall 4 — `overflow-x-auto` + `min-w-[200px]`), comparison visible on resolved threads (Pitfall 8), server endpoint for comparison deltas (architecture anti-pattern)
|
||||
|
||||
### Phase 4: Setup Impact Preview
|
||||
|
||||
**Rationale:** No schema dependency. Easiest to build last because the comparison view UI (Phase 3) already establishes the thread header area where the setup selector lives. Both add-mode and replace-mode deltas must be designed here to avoid the misleading pure-addition delta.
|
||||
**Delivers:** Setup selector dropdown in thread header (`useSetups()` data); `SetupImpactRow.tsx` component; `impactSetupId` in `uiStore`; add-mode delta and replace-mode delta (auto-defaults to replace when same-category item exists in setup); null weight guard ("-- (no weight data)" not "+0g"); unit-aware display via `useWeightUnit()` / `useCurrency()`
|
||||
**Uses:** Existing `useSetup(id)` hook (no new API), existing `formatWeight` / `formatPrice` formatters, `categoryId` on thread for replacement item detection
|
||||
**Avoids:** Stale data in impact preview (Pitfall 3 — reactive `useQuery` for setup data), wrong delta from add-vs-replace confusion (Pitfall 6), null weight treated as 0 (integration gotcha), server endpoint for delta calculation (architecture anti-pattern)
|
||||
### Phase 6: Community Features (Reviews, Profiles, Discovery)
|
||||
**Rationale:** With the global item catalog seeded and users scoped, community features can be built on top. These three feature areas are bundled because they form the public-facing value proposition together — profiles without discovery, or discovery without content, delivers nothing meaningful to users.
|
||||
**Delivers:** Structured reviews (overall + dimension ratings, one per user per global item, no freeform text); user public profiles (display name, avatar, bio, joined date, public setups list); public setup detail pages; discovery browse page (recent public setups, recently reviewed items, popular items by owner count); item detail pages with aggregated stats (owner count, average ratings, crowd-verified weight)
|
||||
**Addresses:** Structured reviews, user profiles, public setup pages, item detail pages, discovery browse (FEATURES.md P1)
|
||||
**Uses:** Review schema with composite unique `(userId, globalItemId)`; denormalized `avgRating` and `ownerCount` on globalItems; cursor-paginated discovery queries; indexes on `(is_public, updated_at)` and `owner_count`
|
||||
**Avoids:** Pitfall 9 (private by default enforced in all discovery queries); N+1 query trap in feed (use joins, not per-item queries)
|
||||
**Research flag:** Discovery feed pagination (cursor vs. offset) and feed composition are well-documented standard patterns at this scale. No additional research needed for v2.0.
|
||||
|
||||
### Phase Ordering Rationale
|
||||
|
||||
- Phase 1 before all others: SQLite schema changes batched into a single migration; test helper updated once; pros/cons in edit panel adds value immediately without waiting for the comparison view
|
||||
- Phase 2 before Phase 3: rank data (sort order, rank badge) is more valuable displayed in the comparison table than in the card grid alone; building the comparison view after ranking ensures the table is complete on first delivery
|
||||
- Phase 3 before Phase 4: comparison view establishes the thread header chrome (toggle button area) where the setup selector in Phase 4 will live; building header UI in Phase 3 reduces Phase 4 scope
|
||||
- Phases 3 and 4 are technically independent and could parallelize, but sequencing them keeps the thread detail header changes contained to one phase at a time
|
||||
- Phases 1-3 form an unbreakable dependency chain: each phase is a prerequisite for the next. No parallelization is possible without creating rework.
|
||||
- Phase 4 (images) is inserted before community features because public profiles and discovery serve images to unauthenticated users — local filesystem is not viable at that point.
|
||||
- Phase 5 (global items) must precede Phase 6 (community features) because reviews require global item records to attach to; the dependency is one-directional.
|
||||
- Phases 5 and 6 could be split across releases (global items + linking as v2.0, community features as v2.1) if schedule pressure exists — the global item catalog delivers standalone value through item linking even before reviews exist.
|
||||
|
||||
### Research Flags
|
||||
|
||||
Phases that need careful plan review before execution (not full research-phase, but plan must address specific design decisions):
|
||||
- **Phase 2:** The `tempItems` local state pattern and fractional `sort_order` arithmetic are non-obvious. The PLAN.md must spell these out explicitly before coding. PITFALLS.md Pitfall 1 and Pitfall 2 must be addressed in the plan, not discovered during implementation.
|
||||
- **Phase 4:** The add-vs-replace distinction requires deliberate design (which mode is default, how replacement item is detected by category, how null weight is surfaced). PITFALLS.md Pitfall 6 must be resolved in the plan before the component is built.
|
||||
Needs deeper research during planning:
|
||||
- **Phase 2 (Auth):** Auth provider decision (Logto vs. Authentik) must be resolved before this phase is planned. The integration pattern differs significantly between the two options — React SDK + backend JWT validation (Logto) vs. server-side middleware only (Authentik).
|
||||
- **Phase 5 (Global Items):** Postgres full-text search index design and query tuning for the global item catalog. The schema is specified; the FTS configuration (`tsvector` column type, GIN index, query parser) needs validation against expected query patterns and initial catalog size.
|
||||
|
||||
Phases with standard patterns (can skip `/gsd:research-phase`):
|
||||
- **Phase 1:** Standard Drizzle migration + Zod schema extension; established patterns in the codebase; ARCHITECTURE.md provides exact column definitions
|
||||
- **Phase 3:** Pure presentational component; Tailwind comparison table is well-documented; ARCHITECTURE.md provides complete component structure, props interface, and delta calculation code
|
||||
Phases with standard patterns (skip research-phase):
|
||||
- **Phase 1 (Database):** Migration path is thoroughly documented across ARCHITECTURE.md, PITFALLS.md, and STACK.md. Per-file implementation checklist is complete.
|
||||
- **Phase 3 (Multi-user):** Per-table userId requirements are fully specified. Pitfall checklist covers all edge cases including MCP tools, thread resolution, settings, and the threadCandidates join path.
|
||||
- **Phase 4 (Images):** Bun S3Client + MinIO is documented by the Bun team. Standard proxy-then-presigned-URL migration path is specified.
|
||||
- **Phase 6 (Community):** Schema, services, and query patterns are fully specified in ARCHITECTURE.md. No novel patterns.
|
||||
|
||||
## Confidence Assessment
|
||||
|
||||
| Area | Confidence | Notes |
|
||||
|------|------------|-------|
|
||||
| Stack | HIGH | Verified from `bun.lock` (framer-motion React 19 peerDeps confirmed); dnd-kit abandonment verified via npm + GitHub; Motion Reorder API verified via motion.dev docs |
|
||||
| Features | HIGH | Codebase analysis confirmed no rank/pros/cons columns in existing schema; NNGroup + Smashing Magazine for comparison UX patterns; competitor analysis (LighterPack, GearGrams, OutPack) confirmed feature gap |
|
||||
| Architecture | HIGH | Full integration map derived from direct codebase analysis; build order confirmed by column dependency graph; all changed files enumerated (3 new, 10 modified); complete code patterns provided |
|
||||
| Pitfalls | HIGH | dnd-kit flicker: verified in GitHub Discussion #1522 and Issue #921; fractional indexing: verified via steveruiz.me and fractional-indexing library; comparison UX: Baymard Institute and NNGroup |
|
||||
| Stack | HIGH | All recommendations backed by official docs. Known issue #4122 (drizzle-kit + Bun SQL) is documented with a clear workaround. The Logto vs. Authentik disagreement is an open decision requiring resolution, not a confidence gap — both options are well-validated. |
|
||||
| Features | HIGH | Competitor analysis is comprehensive (LighterPack, GearGrams, Trailspace, MyGear). Feature dependency chain is fully mapped. P1/P2/P3 prioritization is grounded in implementation cost and dependency analysis. |
|
||||
| Architecture | HIGH | Based on direct codebase analysis of GearBox v1.4 plus official Drizzle and Hono docs. Component-level change inventory is complete (new files, modified files, removed files enumerated). Data flow diagrams are concrete and code-level. |
|
||||
| Pitfalls | HIGH | 12 pitfalls, each with specific GearBox v1.4 codebase context (file names, function names, column names, specific patterns). Confidence is high because research analyzed the actual codebase, not generic migration advice. |
|
||||
|
||||
**Overall confidence:** HIGH
|
||||
|
||||
### Gaps to Address
|
||||
|
||||
- **Impact preview add-vs-replace UX:** Research establishes that both modes are needed and when to default to each (same-category item in setup = replace mode). The exact affordance — dropdown to select which item is replaced vs. automatic category matching — is not fully specified. Recommendation: auto-match by category with a "change" link to override. Decide during Phase 4 planning.
|
||||
- **Comparison view maximum candidate count:** Research recommends 3-4 max for usability. GearBox has no current limit on candidates per thread. Whether to enforce a hard display limit (hide additional candidates behind "show more") or allow unrestricted horizontal scroll should be decided during Phase 3 planning.
|
||||
- **Sort order initialization for existing candidates:** When the migration runs, existing `thread_candidates` rows get `sort_order = 0` (default). Phase 1 plan must specify whether to initialize existing candidates with spaced values (e.g., 1000, 2000, 3000) at migration time or accept that all existing rows start at 0 and rely on first drag to establish order.
|
||||
- **Open decision — auth provider (Logto vs. Authentik):** Must be resolved before Phase 2 planning begins. Resolution criteria: (1) If the project plans to use the auth provider for infrastructure SSO beyond the GearBox app (Portainer, Grafana, Gitea, etc.), choose Authentik — it handles proxy-mode SSO that Logto does not. (2) If GearBox is the only app needing auth, choose Logto — simpler infrastructure (no Redis dependency), React SDK eliminates manual OIDC redirect handling, TypeScript-native. STACK.md's Logto recommendation is correct for the app-auth-only use case.
|
||||
|
||||
- **Review dimension configuration tension:** FEATURES.md specifies "3-5 dimension ratings per product category, admin-configurable" (flexible `reviewDimensions` table with `categoryId` FK). ARCHITECTURE.md uses hardcoded columns (`weightRating`, `durabilityRating`, `valueRating`). For v2.0, use the hardcoded column approach (simpler, no dimension management UI needed). The flexible schema is a v2.x concern. This must be noted explicitly in Phase 6 planning to prevent scope creep.
|
||||
|
||||
- **E2E test authentication strategy post-auth migration:** PITFALLS.md recommends switching E2E tests to API key authentication after auth moves external. The mechanism for creating a test user in the new auth system (direct Postgres insert bypassing the OIDC provider vs. auth provider admin API) needs to be decided during Phase 2 planning.
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- `bun.lock` (project lockfile) — framer-motion v12.37.0 peerDeps `"react: ^18.0.0 || ^19.0.0"` confirmed
|
||||
- [Motion Reorder docs](https://motion.dev/docs/react-reorder) — `Reorder.Group`, `Reorder.Item`, `onDragEnd` API
|
||||
- [dnd-kit Discussion #1522](https://github.com/clauderic/dnd-kit/discussions/1522) — `tempItems` solution for React Query cache flicker
|
||||
- [dnd-kit Issue #921](https://github.com/clauderic/dnd-kit/issues/921) — root cause of state lifecycle mismatch
|
||||
- [Fractional Indexing — steveruiz.me](https://www.steveruiz.me/posts/reordering-fractional-indices) — why float sort keys beat integer reorder for databases
|
||||
- [Baymard Institute: Comparison Tool Design](https://baymard.com/blog/user-friendly-comparison-tools) — sticky headers, horizontal scroll, minimum column width
|
||||
- [NNGroup: Comparison Tables](https://www.nngroup.com/articles/comparison-tables/) — information architecture, anti-patterns
|
||||
- [Smashing Magazine: Feature Comparison Table](https://www.smashingmagazine.com/2017/08/designing-perfect-feature-comparison-table/) — table layout patterns
|
||||
- GearBox codebase direct analysis (`src/db/schema.ts`, `src/server/services/`, `src/client/hooks/`, `tests/helpers/db.ts`) — confirmed existing patterns, missing columns, integration points
|
||||
- GearBox v1.4 codebase — Direct analysis of `src/db/schema.ts`, service files, auth middleware, MCP server, test helpers, `db/index.ts`, E2E seed (direct codebase reference)
|
||||
- [Logto official docs — React quickstart](https://docs.logto.io/quick-starts/react) — SDK setup, LogtoProvider config
|
||||
- [Logto API protection — JWT validation](https://docs.logto.io/api-protection/nodejs/express) — jose-based middleware pattern
|
||||
- [Logto OSS getting started](https://docs.logto.io/logto-oss/get-started-with-oss) — Docker deployment, Postgres requirements
|
||||
- [Drizzle ORM — Bun SQL driver](https://orm.drizzle.team/docs/connect-bun-sql) — Native Postgres via Bun
|
||||
- [Drizzle ORM — PostgreSQL column types](https://orm.drizzle.team/docs/column-types/pg) — pg-core schema definitions
|
||||
- [Bun S3 documentation](https://bun.com/docs/runtime/s3) — Native S3 client, MinIO config
|
||||
- [jose GitHub](https://github.com/panva/jose) — JWT library v6.2.2, explicit Bun support
|
||||
- [postgres.js npm](https://www.npmjs.com/package/postgres) — v3.4.8, fallback driver
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- [@dnd-kit/core npm](https://www.npmjs.com/package/@dnd-kit/core) — v6.3.1 last published ~1 year ago, no React 19
|
||||
- [dnd-kit React 19 issue #1511](https://github.com/clauderic/dnd-kit/issues/1511) — CLOSED but React 19 TypeScript issues confirmed
|
||||
- [@dnd-kit/react roadmap discussion #1842](https://github.com/clauderic/dnd-kit/discussions/1842) — 0 maintainer replies; pre-1.0 risk signal
|
||||
- [hello-pangea/dnd React 19 issue #864](https://github.com/hello-pangea/dnd/issues/864) — still open as of Jan 2026
|
||||
- [BrightCoding dnd-kit deep dive (2025)](https://www.blog.brightcoding.dev/2025/08/21/the-ultimate-drag-and-drop-toolkit-for-react-a-deep-dive-into-dnd-kit/) — react-beautiful-dnd abandoned; dnd-kit current standard but React 19 gap confirmed
|
||||
- [TrailsMag: Leaving LighterPack](https://trailsmag.net/blogs/hiker-box/ultralight-the-gear-tracking-app-i-m-leaving-lighterpack-for) — LighterPack feature gap analysis
|
||||
- [Contentsquare: Comparing products UX](https://contentsquare.com/blog/comparing-products-design-practices-to-help-your-users-avoid-fragmented-comparison-7/) — fragmented comparison pitfalls
|
||||
- [drizzle-kit Bun SQL issue #4122](https://github.com/drizzle-team/drizzle-orm/issues/4122) — Known CLI limitation with Bun driver
|
||||
- [Authentik vs Zitadel comparison](https://wz-it.com/en/blog/authentik-vs-zitadel-identity-provider-comparison/) — Auth provider tradeoff analysis
|
||||
- [Keycloak vs Authentik vs Zitadel 2026](https://blog.houseoffoss.com/post/keycloak-vs-authentik-vs-zitadel-2026-which-open-source-login-tool-should-you-use) — Ecosystem overview
|
||||
- [LighterPack](https://lighterpack.com/), [GearGrams](https://www.geargrams.com/), [Trailspace](https://www.trailspace.com/), [MyGear](https://mygear.world/) — Competitor feature analysis
|
||||
- [Multi-tenant architecture guide (WorkOS)](https://workos.com/blog/developers-guide-saas-multi-tenant-architecture) — Multi-user data isolation patterns
|
||||
- [SQLite to PostgreSQL migration pitfalls (Open WebUI)](https://github.com/open-webui/open-webui/discussions/21609) — Migration risk validation
|
||||
- [How to migrate from SQLite to PostgreSQL (Render)](https://render.com/articles/how-to-migrate-from-sqlite-to-postgresql) — Data migration script patterns
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
- [Fractional Indexing SQLite library](https://github.com/sqliteai/fractional-indexing) — implementation reference for lexicographic sort keys (pattern reference only; direct float arithmetic sufficient for this use case)
|
||||
- [Top 5 Drag-and-Drop Libraries for React 2026](https://puckeditor.com/blog/top-5-drag-and-drop-libraries-for-react) — ecosystem overview confirming dnd-kit and hello-pangea/dnd limitations
|
||||
- [Drizzle ORM PostgreSQL best practices 2025 (GitHub Gist)](https://gist.github.com/productdevbook/7c9ce3bbeb96b3fabc3c7c2aa2abc717) — Schema patterns (validate against official docs during implementation)
|
||||
- [GetStream Social Feed Architecture](https://getstream.io/blog/social-media-feed/) — Feed implementation patterns referenced for anti-patterns to avoid
|
||||
|
||||
---
|
||||
*Research completed: 2026-03-16*
|
||||
*Research completed: 2026-04-03*
|
||||
*Ready for roadmap: yes*
|
||||
|
||||
Reference in New Issue
Block a user