docs: complete project research

This commit is contained in:
2026-03-14 21:46:42 +01:00
parent 632e45d294
commit 8f1647d557
5 changed files with 1246 additions and 0 deletions

View File

@@ -0,0 +1,362 @@
# Architecture Research
**Domain:** Single-user gear management and purchase planning web app
**Researched:** 2026-03-14
**Confidence:** HIGH
## Standard Architecture
### System Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ Client (Browser) │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Dashboard │ │Collection │ │ Threads │ │ Setups │ │
│ │ Page │ │ Page │ │ Page │ │ Page │ │
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
│ │ │ │ │ │
│ ┌─────┴──────────────┴──────────────┴──────────────┴─────┐ │
│ │ Shared UI Components │ │
│ │ (ItemCard, ComparisonTable, WeightBadge, CostBadge) │ │
│ └────────────────────────┬───────────────────────────────┘ │
│ │ fetch() │
├───────────────────────────┼────────────────────────────────────┤
│ Bun.serve() │
│ ┌────────────────────────┴───────────────────────────────┐ │
│ │ API Routes Layer │ │
│ │ /api/items /api/threads /api/setups │ │
│ │ /api/stats /api/candidates /api/images │ │
│ └────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌────────────────────────┴───────────────────────────────┐ │
│ │ Service Layer │ │
│ │ ItemService ThreadService SetupService StatsService│ │
│ └────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌────────────────────────┴───────────────────────────────┐ │
│ │ Data Access (Drizzle ORM) │ │
│ │ Schema + Queries │ │
│ └────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌────────────────────────┴───────────────────────────────┐ │
│ │ SQLite (bun:sqlite) │ │
│ │ gearbox.db file │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
This is a monolithic full-stack app running on a single Bun process. No microservices, no separate API server, no Docker. Bun's built-in fullstack dev server handles both static asset bundling and API routes from a single `Bun.serve()` call. SQLite is the database -- embedded, zero-config, accessed through Bun's native `bun:sqlite` module (3-6x faster than better-sqlite3).
### Component Responsibilities
| Component | Responsibility | Typical Implementation |
|-----------|----------------|------------------------|
| Dashboard Page | Entry point, summary cards, navigation | React page showing item count, active threads, setup stats |
| Collection Page | CRUD for gear items, filtering, sorting | React page with list/grid views, item detail modal |
| Threads Page | Purchase research threads with candidates | React page with thread list, candidate comparison view |
| Setups Page | Compose named setups from collection items | React page with drag/drop or select-to-add from collection |
| API Routes | HTTP endpoints for all data operations | Bun.serve() route handlers, REST-style |
| Service Layer | Business logic, calculations (weight/cost totals) | TypeScript modules with domain logic |
| Data Access | Schema definition, queries, migrations | Drizzle ORM with SQLite dialect |
| SQLite DB | Persistent storage | Single file, bun:sqlite native module |
| Image Storage | Photo uploads for gear items | Local filesystem (`./uploads/`) served as static files |
## Recommended Project Structure
```
src/
├── index.tsx # Bun.serve() entry point, route registration
├── pages/ # HTML entrypoints for each page
│ ├── index.html # Dashboard
│ ├── collection.html # Collection page
│ ├── threads.html # Planning threads page
│ └── setups.html # Setups page
├── client/ # React frontend code
│ ├── components/ # Shared UI components
│ │ ├── ItemCard.tsx
│ │ ├── WeightBadge.tsx
│ │ ├── CostBadge.tsx
│ │ ├── ComparisonTable.tsx
│ │ ├── StatusBadge.tsx
│ │ └── Layout.tsx
│ ├── pages/ # Page-level React components
│ │ ├── Dashboard.tsx
│ │ ├── Collection.tsx
│ │ ├── ThreadList.tsx
│ │ ├── ThreadDetail.tsx
│ │ ├── SetupList.tsx
│ │ └── SetupDetail.tsx
│ ├── hooks/ # Custom React hooks
│ │ ├── useItems.ts
│ │ ├── useThreads.ts
│ │ └── useSetups.ts
│ └── lib/ # Client utilities
│ ├── api.ts # Fetch wrapper for API calls
│ └── formatters.ts # Weight/cost formatting helpers
├── server/ # Backend code
│ ├── routes/ # API route handlers
│ │ ├── items.ts
│ │ ├── threads.ts
│ │ ├── candidates.ts
│ │ ├── setups.ts
│ │ ├── images.ts
│ │ └── stats.ts
│ └── services/ # Business logic
│ ├── item.service.ts
│ ├── thread.service.ts
│ ├── setup.service.ts
│ └── stats.service.ts
├── db/ # Database layer
│ ├── schema.ts # Drizzle table definitions
│ ├── index.ts # Database connection singleton
│ ├── seed.ts # Optional dev seed data
│ └── migrations/ # Drizzle Kit generated migrations
├── shared/ # Types shared between client and server
│ └── types.ts # Item, Thread, Candidate, Setup types
uploads/ # Gear photos (gitignored, outside src/)
drizzle.config.ts # Drizzle Kit config
```
### Structure Rationale
- **`client/` and `server/` separation:** Clear boundary between browser code and server code. Both import from `shared/` and `db/` (server only) but never from each other.
- **`pages/` HTML entrypoints:** Bun's fullstack server uses HTML files as route entrypoints. Each HTML file imports its corresponding React component tree.
- **`server/routes/` + `server/services/`:** Routes handle HTTP concerns (parsing params, status codes). Services handle business logic (calculating totals, validating state transitions). This prevents bloated route handlers.
- **`db/schema.ts` as single source of truth:** All table definitions in one file. Drizzle infers TypeScript types from the schema, so types flow from DB to API to client.
- **`shared/types.ts`:** API response types and domain enums shared between client and server. Avoids type drift.
- **`uploads/` outside `src/`:** User-uploaded images are not source code. Served as static files by Bun.
## Architectural Patterns
### Pattern 1: Bun Fullstack Monolith
**What:** Single Bun.serve() process serves HTML pages, bundled React assets, and API routes. No separate frontend dev server, no proxy config, no CORS.
**When to use:** Single-user apps, prototypes, small team projects where deployment simplicity matters.
**Trade-offs:** Extremely simple to deploy (one process, one command), but no horizontal scaling. For GearBox this is ideal -- single user, no scaling needed.
**Example:**
```typescript
// src/index.tsx
import homepage from "./pages/index.html";
import collectionPage from "./pages/collection.html";
import { itemRoutes } from "./server/routes/items";
import { threadRoutes } from "./server/routes/threads";
Bun.serve({
routes: {
"/": homepage,
"/collection": collectionPage,
...itemRoutes,
...threadRoutes,
},
development: true,
});
```
### Pattern 2: Service Layer for Business Logic
**What:** Route handlers delegate to service modules that contain domain logic. Services are pure functions or classes that take data in and return results, with no HTTP awareness.
**When to use:** When routes would otherwise contain calculation logic (weight totals, cost impact analysis, status transitions).
**Trade-offs:** Slightly more files, but logic is testable without HTTP mocking and reusable across routes.
**Example:**
```typescript
// server/services/setup.service.ts
export function calculateSetupTotals(items: Item[]): SetupTotals {
return {
totalWeight: items.reduce((sum, i) => sum + (i.weightGrams ?? 0), 0),
totalCost: items.reduce((sum, i) => sum + (i.priceCents ?? 0), 0),
itemCount: items.length,
};
}
export function computeCandidateImpact(
setup: Setup,
candidate: Candidate
): Impact {
const currentTotals = calculateSetupTotals(setup.items);
return {
weightDelta: (candidate.weightGrams ?? 0) - (setup.replacingItem?.weightGrams ?? 0),
costDelta: (candidate.priceCents ?? 0) - (setup.replacingItem?.priceCents ?? 0),
newTotalWeight: currentTotals.totalWeight + this.weightDelta,
newTotalCost: currentTotals.totalCost + this.costDelta,
};
}
```
### Pattern 3: Drizzle ORM with bun:sqlite
**What:** Drizzle provides type-safe SQL query building and schema-as-code migrations on top of Bun's native SQLite. Schema definitions double as TypeScript type sources.
**When to use:** Any Bun + SQLite project that wants type safety without the overhead of a full ORM like Prisma.
**Trade-offs:** Lightweight (no query engine, no runtime overhead). SQL-first philosophy means you write SQL-like code, not abstract methods. Migration tooling via Drizzle Kit is solid but simpler than Prisma Migrate.
**Example:**
```typescript
// db/schema.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const items = sqliteTable("items", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
category: text("category"),
weightGrams: integer("weight_grams"),
priceCents: integer("price_cents"),
purchaseSource: text("purchase_source"),
productUrl: text("product_url"),
notes: text("notes"),
imageFilename: text("image_filename"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
});
```
## Data Flow
### Request Flow
```
[User clicks "Add Item"]
|
[React component] --> fetch("/api/items", { method: "POST", body })
|
[Bun.serve route handler] --> validates input, calls service
|
[ItemService.create()] --> business logic, defaults
|
[Drizzle ORM] --> db.insert(items).values(...)
|
[bun:sqlite] --> writes to gearbox.db
|
[Response] <-- { id, name, ... } JSON <-- 201 Created
|
[React state update] --> re-renders item list
```
### Key Data Flows
1. **Collection CRUD:** Straightforward REST. Client sends item data, server validates, persists, returns updated item. Client hooks (useItems) manage local state.
2. **Thread lifecycle:** Create thread -> Add candidates -> Compare -> Resolve (pick winner). Resolution triggers: candidate becomes a collection item, thread status changes to "resolved", other candidates marked as rejected. This is the most stateful flow.
3. **Setup composition:** User selects items from collection to add to a named setup. Server calculates aggregate weight/cost. When viewing a thread candidate, "impact on setup" is computed by comparing candidate against current setup totals (or against a specific item being replaced).
4. **Dashboard aggregation:** Dashboard fetches summary stats via `/api/stats` -- total items, total collection value, active threads count, setup count. This is a read-only aggregation endpoint, not a separate data store.
5. **Image upload:** Multipart form upload to `/api/images`, saved to `./uploads/` with a UUID filename. The filename is stored on the item record. Images served as static files.
### Data Model Relationships
```
items (gear collection)
|
|-- 1:N --> setup_items (junction) <-- N:1 -- setups
|
|-- 1:N --> thread_candidates (when resolved, candidate -> item)
threads (planning threads)
|
|-- 1:N --> candidates (potential purchases)
|-- status: researching | ordered | arrived
|-- resolved_as: winner | rejected | null
setups
|
|-- N:M --> items (via setup_items junction table)
```
### State Management
No global state library needed. React hooks + fetch are sufficient for a single-user app with this complexity level.
```
[React Hook per domain] [API call] [Server] [SQLite]
useItems() state --------> GET /api/items --> route handler --> SELECT
| |
|<-- setItems(data) <--- JSON response <--- query result <------+
```
Each page manages its own state via custom hooks (`useItems`, `useThreads`, `useSetups`). No Redux, no Zustand. If a mutation on one page affects another (e.g., resolving a thread adds an item to collection), the target page simply refetches on mount.
## Scaling Considerations
| Scale | Architecture Adjustments |
|-------|--------------------------|
| Single user (GearBox) | SQLite + single Bun process. Zero infrastructure. This is the target. |
| 1-10 users | Still fine with SQLite in WAL mode. Add basic auth if needed. |
| 100+ users | Switch to PostgreSQL, add connection pooling, consider separate API server. Not relevant for this project. |
### Scaling Priorities
1. **First bottleneck:** Image storage. If users upload many high-res photos, disk fills up. Mitigation: resize on upload (sharp library), limit file sizes.
2. **Second bottleneck:** SQLite write contention under concurrent access. Not a concern for single-user. If it ever matters, switch to WAL mode or PostgreSQL.
## Anti-Patterns
### Anti-Pattern 1: Storing Money as Floats
**What people do:** Use `float` or JavaScript `number` for prices (e.g., `19.99`).
**Why it's wrong:** Floating point arithmetic causes rounding errors. `0.1 + 0.2 !== 0.3`. Price calculations silently drift.
**Do this instead:** Store prices as integers in cents (`1999` for $19.99). Format for display only in the UI layer. The schema uses `priceCents: integer`.
### Anti-Pattern 2: Overengineering State Management
**What people do:** Install Redux/Zustand/Jotai for a single-user CRUD app, create elaborate store slices, actions, reducers.
**Why it's wrong:** Adds complexity with zero benefit when there is one user and no shared state across tabs or real-time updates.
**Do this instead:** Use React hooks with fetch. `useState` + `useEffect` + a thin API wrapper. Refetch on mount. Keep it boring.
### Anti-Pattern 3: SPA with Client-Side Routing for Everything
**What people do:** Build a full SPA with React Router, lazy loading, code splitting for 4-5 pages.
**Why it's wrong:** Bun's fullstack server already handles page routing via HTML entrypoints. Adding client-side routing means duplicating routing logic, losing Bun's built-in asset optimization per page, and adding bundle complexity.
**Do this instead:** Use Bun's HTML-based routing. Each page is a separate HTML entrypoint with its own React tree. Navigation between pages is standard `<a href>` links. Keep client-side routing for in-page state (e.g., tabs within thread detail) only.
### Anti-Pattern 4: Storing Computed Aggregates in the Database
**What people do:** Store `totalWeight` and `totalCost` on the setup record, then try to keep them in sync when items change.
**Why it's wrong:** Stale data, sync bugs, update anomalies. Items get edited but setup totals do not get recalculated.
**Do this instead:** Compute totals on read. SQLite is fast enough for `SUM()` across a handful of items. Calculate in the service layer or as a SQL aggregate. For a single-user app with small datasets, this is effectively instant.
## Integration Points
### External Services
| Service | Integration Pattern | Notes |
|---------|---------------------|-------|
| None for v1 | N/A | Single-user local app, no external APIs needed |
| Product URLs | Outbound links only | Store URLs to retailer pages, no API scraping |
### Internal Boundaries
| Boundary | Communication | Notes |
|----------|---------------|-------|
| Client <-> Server | REST API (JSON over fetch) | No WebSockets needed, no real-time requirements |
| Routes <-> Services | Direct function calls | Same process, no serialization overhead |
| Services <-> Database | Drizzle ORM queries | Type-safe, no raw SQL strings |
| Server <-> Filesystem | Image read/write | `./uploads/` directory for gear photos |
## Build Order (Dependency Chain)
The architecture implies this build sequence:
1. **Database schema + Drizzle setup** -- Everything depends on the data model. Define tables for items, threads, candidates, setups, setup_items first.
2. **API routes for items (CRUD)** -- The core entity. Threads and setups reference items.
3. **Collection UI** -- First visible feature. Validates the data model and API work end-to-end.
4. **Thread + candidate API and UI** -- Depends on items existing to resolve candidates into the collection.
5. **Setup composition API and UI** -- Depends on items existing to compose into setups.
6. **Dashboard** -- Aggregates stats from all other entities. Build last since it reads from everything.
7. **Polish: image upload, impact calculations, status tracking** -- Enhancement layer on top of working CRUD.
This ordering means each phase produces a usable increment: after phase 3 you have a working gear catalog, after phase 4 you can plan purchases, after phase 5 you can compose setups.
## Sources
- [Bun Fullstack Dev Server docs](https://bun.com/docs/bundler/fullstack) -- Official documentation on Bun's HTML-based routing and asset bundling
- [bun:sqlite API Reference](https://bun.com/reference/bun/sqlite) -- Native SQLite module documentation
- [Building Full-Stack App with Bun.js, React and Drizzle ORM](https://awplife.com/building-full-stack-app-with-bun-js-react-drizzle/) -- Project structure reference
- [Bun v3.1 Release (InfoQ)](https://www.infoq.com/news/2026/01/bun-v3-1-release/) -- Zero-config frontend, built-in DB clients
- [Bun + React + Hono pattern](https://dev.to/falconz/serving-a-react-app-and-hono-api-together-with-bun-1gfg) -- Alternative fullstack patterns
- [Inventory Management DB Design (Medium)](https://medium.com/@bhargavkoya56/weekly-db-project-1-inventory-management-db-design-seed-from-schema-design-to-performance-8e6b56445fe6) -- Schema design patterns for inventory systems
---
*Architecture research for: GearBox gear management app*
*Researched: 2026-03-14*

View File

@@ -0,0 +1,201 @@
# Feature Research
**Domain:** Gear management and purchase planning (personal inventory + research workflow)
**Researched:** 2026-03-14
**Confidence:** HIGH
## Feature Landscape
### Table Stakes (Users Expect These)
Features users assume exist. Missing these = product feels incomplete.
| Feature | Why Expected | Complexity | Notes |
|---------|--------------|------------|-------|
| Item CRUD with core fields (name, weight, price, category) | Every gear app and spreadsheet has this. It is the minimum unit of value. | LOW | Weight and price are the two fields users care about most. Category groups items visually. |
| Weight unit support (g, oz, lb, kg) | Gear communities are split between metric and imperial. LighterPack, GearGrams, Hikt all support multi-unit. | LOW | Store in grams internally, display in user-preferred unit. Conversion is trivial. |
| Automatic weight/cost totals | Spreadsheets do this. Every competitor does this. Manual math = why bother with an app. | LOW | Sum by category, by setup, by collection. Real-time recalculation on any change. |
| Categories/grouping | LighterPack, GearGrams, Packstack all organize by category (shelter, sleep, cook, clothing, etc.). Without grouping, lists become unreadable past 20 items. | LOW | User-defined categories. Suggest defaults but allow custom. |
| Named setups / packing lists | LighterPack has lists, GearGrams has lists, Packstack has trips, Hikt has packing lists. Composing subsets of your gear into purpose-specific loadouts is universal. | MEDIUM | Items belong to collection; setups reference items from collection. Many-to-many relationship. |
| Setup weight/cost breakdown | Every competitor shows base weight, worn weight, consumable weight as separate totals. Pie charts or percentage breakdowns by category are standard (LighterPack pioneered this). | MEDIUM | Weight classification (base/worn/consumable) per item per setup. Visual breakdown is expected. |
| Notes/description per item | Spreadsheet users write notes. Every competitor supports free text on items. Useful for fit notes, durability observations, model year specifics. | LOW | Plain text field. No rich text needed for v1. |
| Product links / URLs | Users track where they found or bought items. Spreadsheets always have a "link" column. | LOW | Single URL field per item. |
| Photos per item | Hikt, GearCloset, and Packrat all support item photos. Visual identification matters -- many gear items look similar in text. | MEDIUM | Image upload and storage. Start with one photo per item; multi-photo is a differentiator. |
| Search and filter | Once a collection exceeds 30-40 items, finding things without search is painful. Hikt highlights "searchable digital closet." | LOW | Filter by category, search by name. Basic but essential. |
| Import from CSV | GearGrams, HikeLite, HikerHerd, Packrat all support CSV import. Users migrating from spreadsheets (GearBox's primary audience) need this. | MEDIUM | Define a simple CSV schema. Map columns to fields. Handle unit conversion on import. |
| Export to CSV | Companion to import. Users want data portability and backup ability. | LOW | Straightforward serialization of collection data. |
### Differentiators (Competitive Advantage)
Features that set the product apart. Not required, but valuable.
| Feature | Value Proposition | Complexity | Notes |
|---------|-------------------|------------|-------|
| Purchase planning threads | No competitor has this. LighterPack, GearGrams, Packstack, Hikt are all post-purchase tools. GearBox's core value is the pre-purchase research workflow: create a thread, add candidates, compare, decide, then move the winner to your collection. This is the single biggest differentiator. | HIGH | Thread model with candidate items, status tracking, resolution workflow. This is the app's reason to exist. |
| Impact preview ("how does this affect my setup?") | No competitor shows how a potential purchase changes your overall setup weight/cost. Users currently do this math manually in spreadsheets. Seeing "+120g to base weight, +$85 to total cost" before buying is uniquely valuable. | MEDIUM | Requires linking threads to setups. Calculate delta between current item (if replacing) and candidate. |
| Thread resolution workflow | The lifecycle of "researching -> ordered -> arrived -> in collection" does not exist in any competitor. Closing a thread and promoting the winner to your collection is a novel workflow that mirrors how people actually buy gear. | MEDIUM | Status state machine on thread items. Resolution action that creates/updates collection item. |
| Side-by-side candidate comparison | Wishlist apps let you save items. GearBox lets you compare candidates within a thread on the dimensions that matter (weight, price, notes). Similar to product comparison on retail sites, but for your specific context. | MEDIUM | Comparison view pulling from thread candidates. Highlight differences in weight/price. |
| Priority/ranking within threads | Mark favorites among candidates. Simple but no gear app does this because no gear app has a research/planning concept. | LOW | Numeric rank or star/favorite flag per candidate in a thread. |
| Multi-photo per item | Most competitors support zero or one photo. Multiple photos (product shots, detail shots, in-use shots) add real value for gear tracking. | MEDIUM | Gallery per item. Storage considerations. Defer to v1.x. |
| Weight distribution visualization | LighterPack's pie chart is iconic. A clean, modern version with interactive breakdowns by category adds polish. | MEDIUM | Chart component showing percentage of total weight by category. |
| Hobby-agnostic data model | Competitors are hiking/backpacking-specific. GearBox works for bikepacking, sim racing, photography, cycling, or any collection hobby. The data model uses generic "categories" rather than hardcoded "shelter/sleep/cook." | LOW | Architecture decision more than feature. No hiking-specific terminology baked into the model. |
### Anti-Features (Commonly Requested, Often Problematic)
Features that seem good but create problems.
| Feature | Why Requested | Why Problematic | Alternative |
|---------|---------------|-----------------|-------------|
| Multi-user / social sharing | "Share my setup with friends," "collaborate on packing lists." Hikt Premium has real-time collaboration. | Adds auth, permissions, data isolation, and massive complexity to a single-user app. The PROJECT.md explicitly scopes this out. Premature for v1. | Export/share as read-only link or image in a future version. No auth needed. |
| Price tracking / deal alerts | Wishlist apps (Sortd, WishUpon) track price drops. Seems useful for purchase planning. | Requires scraping or API integrations with retailers. Fragile, maintenance-heavy, legally gray. Completely different product category. | Store the price you found manually. Link to the product page. Users can check prices themselves. |
| Barcode/product database scanning | Hikt has barcode scanning and product database lookup. Seems like it saves time. | Requires maintaining or licensing a product database. Outdoor gear barcodes are inconsistent. Mobile-first feature that does not fit a web-first app. | Manual entry is fine for a collection that grows by 1-5 items per month. Not a data-entry-heavy workflow. |
| Custom comparison parameters | "Let me define which fields to compare (warmth rating, denier, waterproof rating)." | Turns a simple app into a configurable schema builder. Massive complexity for marginal value. PROJECT.md lists this as out of scope for v1. | Use the notes field for specs. Fixed comparison on weight/price covers 80% of use cases. |
| Community gear database / shared catalog | "Browse what other people use," "copy someone's gear list." Hikt has community packing lists. | Requires moderation, data quality controls, user accounts, and content management. Completely different product. | Stay focused on personal inventory. Community features are a different app. |
| Mobile native app | PackLight and Hikt have iOS/Android apps. | Doubles or triples development effort. Web-first serves the use case (gear management is a desk activity, not a trailside activity). PROJECT.md scopes this out. | Responsive web design. Works on mobile browsers for quick lookups. |
| Real-time weather integration | Packstack integrates weather for trip planning. | Requires external API, ongoing costs, and is only relevant to outdoor-specific use cases. GearBox is hobby-agnostic. | Out of scope. Users check weather separately. |
| Automated "what to bring" recommendations | AI/rule-based suggestions based on trip conditions. | Requires domain knowledge per hobby, weather data, user preference modeling. Over-engineered for a personal tool. | Users build their own setups. They know their gear. |
## Feature Dependencies
```
[Item CRUD + Core Fields]
|
+--requires--> [Categories]
|
+--enables---> [Named Setups / Packing Lists]
| |
| +--enables---> [Setup Weight/Cost Breakdown]
| |
| +--enables---> [Impact Preview] (also requires Planning Threads)
|
+--enables---> [Planning Threads]
|
+--enables---> [Candidate Comparison]
|
+--enables---> [Thread Resolution Workflow]
| |
| +--creates---> items in [Collection]
|
+--enables---> [Priority/Ranking]
|
+--enables---> [Status Tracking] (researching -> ordered -> arrived)
[Search & Filter] --enhances--> [Item CRUD] (becomes essential at ~30+ items)
[Import CSV] --populates--> [Item CRUD] (bootstrap for spreadsheet migrants)
[Photos] --enhances--> [Item CRUD] (independent, can add anytime)
[Weight Unit Support] --enhances--> [All weight displays] (must be in from day one)
```
### Dependency Notes
- **Named Setups require Item CRUD:** Setups are compositions of existing collection items. The collection must exist first.
- **Planning Threads require Item CRUD:** Thread candidates have the same data shape as collection items (weight, price, etc.). Reuse the item model.
- **Impact Preview requires both Setups and Threads:** You need a setup to compare against and a thread candidate to evaluate. This is a later-phase feature.
- **Thread Resolution creates Collection Items:** The resolution workflow bridges threads and collection. Both must be stable before resolution logic is built.
- **Import CSV populates Collection:** Import is a bootstrap feature for users migrating from spreadsheets. Should be available early but after the core item model is solid.
## MVP Definition
### Launch With (v1)
Minimum viable product -- what is needed to validate the concept and replace a spreadsheet.
- [ ] Item CRUD with weight, price, category, notes, product link -- the core inventory
- [ ] User-defined categories -- organize items meaningfully
- [ ] Weight unit support (g, oz, lb, kg) -- non-negotiable for gear community
- [ ] Automatic weight/cost totals by category and overall -- the reason to use an app over a text file
- [ ] Named setups with item selection and totals -- compose loadouts from your collection
- [ ] Planning threads with candidate items -- the core differentiator, add candidates you are researching
- [ ] Side-by-side candidate comparison on weight/price -- the payoff of the thread concept
- [ ] Thread resolution (pick a winner, move to collection) -- close the loop
- [ ] Dashboard home page -- clean entry point per PROJECT.md constraints
- [ ] Search and filter on collection -- usability at scale
### Add After Validation (v1.x)
Features to add once core is working and the planning thread workflow is proven.
- [ ] Impact preview ("this candidate adds +120g to your Summer Bikepacking setup") -- requires setups + threads to be stable
- [ ] Status tracking on thread items (researching / ordered / arrived) -- lifecycle tracking
- [ ] Priority/ranking within threads -- mark favorites among candidates
- [ ] Photos per item -- visual identification, one photo per item initially
- [ ] CSV import/export -- migration path from spreadsheets, data portability
- [ ] Weight distribution visualization (pie/bar chart by category) -- polish feature
### Future Consideration (v2+)
Features to defer until product-market fit is established.
- [ ] Multi-photo gallery per item -- storage and UI complexity
- [ ] Shareable read-only links for setups -- lightweight sharing without auth
- [ ] Drag-and-drop reordering in lists and setups -- UX refinement
- [ ] Bulk operations (multi-select, bulk categorize, bulk delete) -- power user feature
- [ ] Dark mode -- common request, low priority for initial launch
- [ ] Item history / changelog (track weight after modifications, price changes) -- advanced tracking
## Feature Prioritization Matrix
| Feature | User Value | Implementation Cost | Priority |
|---------|------------|---------------------|----------|
| Item CRUD with core fields | HIGH | LOW | P1 |
| Categories | HIGH | LOW | P1 |
| Weight unit support | HIGH | LOW | P1 |
| Auto weight/cost totals | HIGH | LOW | P1 |
| Named setups | HIGH | MEDIUM | P1 |
| Planning threads | HIGH | HIGH | P1 |
| Candidate comparison | HIGH | MEDIUM | P1 |
| Thread resolution | HIGH | MEDIUM | P1 |
| Dashboard home | MEDIUM | LOW | P1 |
| Search and filter | MEDIUM | LOW | P1 |
| Impact preview | HIGH | MEDIUM | P2 |
| Status tracking (threads) | MEDIUM | LOW | P2 |
| Priority/ranking (threads) | MEDIUM | LOW | P2 |
| Photos per item | MEDIUM | MEDIUM | P2 |
| CSV import/export | MEDIUM | MEDIUM | P2 |
| Weight visualization charts | MEDIUM | MEDIUM | P2 |
| Multi-photo gallery | LOW | MEDIUM | P3 |
| Shareable links | LOW | MEDIUM | P3 |
| Drag-and-drop reordering | LOW | MEDIUM | P3 |
| Bulk operations | LOW | MEDIUM | P3 |
**Priority key:**
- P1: Must have for launch
- P2: Should have, add when possible
- P3: Nice to have, future consideration
## Competitor Feature Analysis
| Feature | LighterPack | GearGrams | Packstack | Hikt | GearBox (Our Approach) |
|---------|-------------|-----------|-----------|------|------------------------|
| Gear inventory | Per-list only (no central closet) | Central library + lists | Full gear library | Full closet with search | Full collection as central source of truth |
| Weight tracking | Excellent -- base/worn/consumable splits, pie charts | Good -- multi-unit, category totals | Good -- base/worn/consumable | Excellent -- smart insights | Base/worn/consumable with unit flexibility |
| Packing lists / setups | Unlimited lists (web) | Multiple lists via drag-drop | Trip-based (2 free) | 3 free lists, more with premium | Named setups composed from collection |
| Purchase planning | None | None | None | None | Planning threads with candidates, comparison, resolution -- unique |
| Impact analysis | None | None | None | None | Show how a candidate changes setup weight/cost -- unique |
| Photos | None | None | None | Yes | Yes (v1.x) |
| Import/export | None (copy-linked lists only) | CSV import | None mentioned | LighterPack import, CSV | CSV import/export (v1.x) |
| Mobile | No native app (web only, poor mobile UX) | Web only | iOS only | iOS + Android + web | Web-first, responsive design |
| Sharing | Shareable links | None mentioned | Shareable trip links | Community lists, collaboration | Deferred (v2+, read-only links) |
| Hobby scope | Hiking/backpacking only | Hiking/backpacking only | Hiking/backpacking only | Hiking/backpacking only | Any hobby (bikepacking, sim racing, photography, etc.) |
| Pricing | Free | Free | Freemium (2 lists free) | Freemium (3 lists free) | Single-user, no tiers |
| Status | Open source, aging, no mobile | Maintained but dated | Active development | Actively developed, modern | New entrant with unique purchase planning angle |
## Sources
- [LighterPack](https://lighterpack.com/) -- free web-based gear list tool, community standard
- [GearGrams](https://www.geargrams.com/) -- drag-and-drop gear library with multi-unit support
- [Packstack](https://www.packstack.io/) -- trip-centric gear management with weather integration
- [Hikt](https://hikt.app/) -- modern gear manager with mobile apps and community features
- [Hikt Blog: Best Backpacking Gear Apps 2026](https://hikt.app/blog/best-backpacking-gear-apps-2026/) -- competitive comparison
- [HikeLite](https://hikeliteapp.com/) -- ultralight gear management with CSV support
- [Packrat](https://www.packrat.app/) -- iOS/Android gear inventory with CSV/JSON import
- [LighterPack GitHub Issues](https://github.com/galenmaly/lighterpack/issues) -- user feature requests and limitations
- [Palespruce Bikepacking Gear Spreadsheet](http://www.palespruce.com/bikepacking-gear-spreadsheet/) -- spreadsheet workflow GearBox replaces
- [99Boulders Backpacking Gear List Spreadsheet](https://www.99boulders.com/backpacking-gear-list-spreadsheet) -- spreadsheet workflow patterns
---
*Feature research for: Gear management and purchase planning*
*Researched: 2026-03-14*

View File

@@ -0,0 +1,249 @@
# Pitfalls Research
**Domain:** Gear management, collection tracking, purchase planning (single-user web app)
**Researched:** 2026-03-14
**Confidence:** HIGH (domain-specific patterns well-documented across gear community and inventory app space)
## Critical Pitfalls
### Pitfall 1: Unit Handling Treated as Display-Only
**What goes wrong:**
Weight and price values are stored as bare numbers without unit metadata. The app assumes everything is grams or dollars, then breaks when users enter ounces, pounds, kilograms, or foreign currencies. Worse: calculations like "total setup weight" silently produce garbage when items have mixed units. A 200g tent and a 5lb sleeping bag get summed as 205.
**Why it happens:**
In a single-user app it feels safe to skip unit handling -- "I'll just always use grams." But real product specs come in mixed units (manufacturers list in oz, g, kg, lb), and copy-pasting from product pages means mixed data creeps in immediately.
**How to avoid:**
Store all weights in a canonical unit (grams) at write time. Accept input in any unit but convert on save. Store the original unit for display purposes but always compute on the canonical value. Build a simple conversion layer from day one -- it is 20 lines of code now vs. a data migration later.
**Warning signs:**
- Weight field is a plain number input with no unit selector
- No conversion logic exists anywhere in the codebase
- Aggregation functions (total weight) do simple `SUM()` without unit awareness
**Phase to address:**
Phase 1 (Data model / Core CRUD) -- unit handling must be in the schema from the start. Retrofitting requires migrating every existing item.
---
### Pitfall 2: Rigid Category Hierarchy Instead of Flexible Tagging
**What goes wrong:**
The app ships with a fixed category tree (Shelter > Tents > 1-Person Tents) that works for bikepacking but fails for sim racing gear, photography equipment, or any other hobby. Users cannot create categories, and items that span categories (a jacket that is both "clothing" and "rain gear") get awkwardly forced into one slot. The "generic enough for any hobby" goal from PROJECT.md dies on contact with a rigid hierarchy.
**Why it happens:**
Hierarchical categories feel structured and "correct" during design. Flat tags feel messy. But hierarchies require knowing the domain upfront, and GearBox explicitly needs to support arbitrary hobbies.
**How to avoid:**
Use a flat tag/label system as the primary organization mechanism. Users create their own tags ("bikepacking", "sleep-system", "cook-kit"). An item can have multiple tags. Optionally allow a single "category" field for broad grouping, but do not enforce hierarchy. Tags are the flexible axis; a single category field is the structured axis.
**Warning signs:**
- Schema has a `category_id` foreign key to a `categories` table with `parent_id`
- Seed data contains a pre-built category tree
- Adding a new hobby requires modifying the database
**Phase to address:**
Phase 1 (Data model) -- this is a schema-level decision. Changing from hierarchy to tags after data exists requires migration of every item's categorization.
---
### Pitfall 3: Planning Thread State Machine Complexity Explosion
**What goes wrong:**
Thread items have statuses (researching, ordered, arrived) plus a thread-level resolution (pick winner, close thread, move to collection). Developers build these as independent fields without modeling the valid state transitions, leading to impossible states: an item marked "arrived" in a thread that was "cancelled," or a "winner" that was never "ordered." The UI then needs defensive checks everywhere, and bugs appear as ghost items in the collection.
**Why it happens:**
Status tracking looks simple -- it is just a string field. But the combination of item-level status + thread-level lifecycle + the "move winner to collection" side effect creates a state machine with many transitions, and without explicit modeling, invalid states are reachable.
**How to avoid:**
Model the thread lifecycle as an explicit state machine with defined transitions. Document which item statuses are valid in each thread state. The "resolve thread" action should be a single transaction that: (1) validates the winner exists, (2) creates the collection item, (3) marks the thread as resolved, (4) updates the thread item status. Use a state diagram during design, not just field definitions.
**Warning signs:**
- Thread status and item status are independent string/enum fields with no transition validation
- No transaction wrapping the "resolve thread + create collection item" flow
- UI shows impossible combinations (resolved thread with "researching" items)
**Phase to address:**
Phase 2 (Planning threads) -- design the state machine before writing any thread code. Do not add statuses incrementally.
---
### Pitfall 4: Image Storage Strategy Causes Data Loss or Bloat
**What goes wrong:**
Two failure modes: (A) Images stored as file paths break when files are moved, deleted, or the app directory changes. Dangling references show broken image icons everywhere. (B) Images stored as BLOBs in SQLite bloat the database, slow down backups, and make the DB file unwieldy as the collection grows.
**Why it happens:**
Image storage seems like a simple problem. File paths are the obvious approach but create a coupling between database records and filesystem state. BLOBs seem self-contained but do not scale with photo-heavy collections.
**How to avoid:**
Store images in a dedicated directory within the app's data folder (e.g., `data/images/{item-id}/`). Store relative paths in the database (never absolute). Generate deterministic filenames from item ID + timestamp to avoid collisions. On item deletion, clean up the image directory. For thumbnails under 100KB, SQLite BLOBs are actually 35% faster than filesystem reads, so consider storing thumbnails as BLOBs while keeping full-size images on disk.
**Warning signs:**
- Absolute file paths in the database
- No cleanup logic when items are deleted (orphaned images accumulate)
- Database file growing much larger than expected (images stored as BLOBs)
- No fallback/placeholder when an image file is missing
**Phase to address:**
Phase 1 (Core CRUD with item photos) -- image handling must be decided before any photos are stored. Migrating image storage strategy later requires moving files and updating every record.
---
### Pitfall 5: Setup Composition Breaks on Collection Changes
**What goes wrong:**
A setup ("Summer Bikepacking") references items from the collection. When an item is deleted from the collection, updated, or replaced via a planning thread resolution, the setup silently breaks -- showing stale data, missing items, or incorrect totals. The user's carefully composed setup becomes untrustworthy.
**Why it happens:**
Setups are modeled as a simple join table (setup_id, item_id) without considering what happens when the item side changes. The relationship is treated as static when it is actually dynamic.
**How to avoid:**
Use foreign keys with explicit `ON DELETE` behavior (not CASCADE -- that silently removes setup entries). When an item is deleted, mark the setup-item link as "removed" and show a visual indicator in the setup view ("1 item no longer in collection"). When a planning thread resolves and replaces an item, offer to update setups that contained the old item. Setups should always recompute totals from live item data, never cache them.
**Warning signs:**
- Setup totals are stored as columns rather than computed from item data
- No foreign key constraints between setups and items
- Deleting a collection item does not check if it belongs to any setup
- No UI indication when a setup references a missing item
**Phase to address:**
Phase 3 (Setups) -- but the foreign key design must be planned in Phase 1 when the items table is created. The item schema needs to anticipate setup references.
---
### Pitfall 6: Comparison View That Does Not Actually Help Decisions
**What goes wrong:**
The side-by-side comparison in planning threads shows raw data (weight: 450g, price: $120) without context. Users cannot see at a glance which candidate is lighter, cheaper, or how each compares to what they already own. The comparison becomes a formatted table, not a decision tool. Users go back to their spreadsheet because it was easier to add formulas.
**Why it happens:**
Building a comparison view that displays data is easy. Building one that surfaces insights ("this is 30% lighter than your current tent but costs 2x more") requires computing deltas against the existing collection, which is a different feature than just showing two items side by side.
**How to avoid:**
Design comparison views to show: (1) absolute values for each candidate, (2) deltas between candidates (highlighted: lighter/heavier, cheaper/more expensive), (3) delta against the current item being replaced from the collection. Use color coding or directional indicators (green down arrow for weight savings, red up arrow for cost increase). This is the core value proposition of GearBox -- do not ship a comparison that is worse than a spreadsheet.
**Warning signs:**
- Comparison view is a static table with no computed differences
- No way to link a thread to "the item I'm replacing" from the collection
- Weight/cost impact on overall setup is not visible from the thread view
**Phase to address:**
Phase 2 (Planning threads) -- comparison is the heart of the thread feature. Build the delta computation alongside the basic thread CRUD, not as a follow-up.
---
## Technical Debt Patterns
Shortcuts that seem reasonable but create long-term problems.
| Shortcut | Immediate Benefit | Long-term Cost | When Acceptable |
|----------|-------------------|----------------|-----------------|
| Caching setup totals in a column | Faster reads, simpler queries | Stale data when items change, bugs when totals disagree with item sum | Never -- always compute from source items |
| Storing currency as float | Simple to implement | Floating point rounding errors in price totals (classic $0.01 bugs) | Never -- use integer cents or a decimal type |
| Skipping "replaced by" links in threads | Simpler thread resolution | Cannot track upgrade history, cannot auto-update setups | Only in earliest prototype, must add before thread resolution ships |
| Hardcoding unit labels | Faster initial development | Cannot support multiple hobbies with different unit conventions (e.g., ml for water bottles) | MVP only if unit conversion layer is planned for next phase |
| Single image per item | Simpler UI and storage | Gear often needs multiple angles, especially for condition tracking | Acceptable for v1 if schema supports multiple images (just limit UI to one) |
## Integration Gotchas
Common mistakes when connecting to external services.
| Integration | Common Mistake | Correct Approach |
|-------------|----------------|------------------|
| Product link scraping | Attempting to auto-fetch product details from URLs, which breaks constantly as sites change layouts | Store the URL as a plain link. Do not scrape. Let users enter details manually. Scraping is a maintenance burden that exceeds its value for a single-user app. |
| Image URLs vs local storage | Hotlinking product images from retailer sites, which break when products are delisted | Always download and store images locally. External URLs rot within months. |
| Export/import formats | Building a custom JSON format that only GearBox understands | Support CSV import/export as the universal fallback. Users are migrating from spreadsheets -- CSV is their native format. |
## Performance Traps
Patterns that work at small scale but fail as usage grows.
| Trap | Symptoms | Prevention | When It Breaks |
|------|----------|------------|----------------|
| Loading all collection items for every setup view | Slow page loads, high memory usage | Paginate collection views; setup views should query only member items | 500+ items in collection |
| Recomputing all setup totals on every item edit | Edit latency increases linearly with number of setups | Only recompute totals for setups containing the edited item | 20+ setups referencing overlapping items |
| Storing full-resolution photos without thumbnails | Page loads become unusably slow when browsing collection | Generate thumbnails on upload; use thumbnails in list views, full images only in detail view | 50+ items with photos |
| Loading all thread candidates for comparison | Irrelevant for small threads, but threads can accumulate many "considered" items | Limit comparison view to 3-4 selected candidates; archive dismissed ones | 15+ candidates in a single thread |
## Security Mistakes
Domain-specific security issues beyond general web security.
| Mistake | Risk | Prevention |
|---------|------|------------|
| No backup mechanism for SQLite database | Single file corruption = total data loss of entire collection | Implement automatic periodic backups (copy the .db file). Provide a manual "export all" button. Single-user apps have no server-side backup by default. |
| Product URLs stored without sanitization | Stored URLs could contain javascript: protocol or XSS payloads if rendered as links | Validate URLs on save (must be http/https). Render with `rel="noopener noreferrer"`. |
| Image uploads without size/type validation | Malicious or accidental upload of huge files or non-image files | Validate file type (accept only jpg/png/webp) and enforce max size (e.g., 5MB) on upload. |
## UX Pitfalls
Common user experience mistakes in this domain.
| Pitfall | User Impact | Better Approach |
|---------|-------------|-----------------|
| Requiring all fields to add an item | Users abandon data entry because they do not know the weight or price yet for items they already own | Only require name. Make weight, price, category, etc. optional. Users fill in details over time. |
| No bulk operations for collection management | Adding 30 existing items one-by-one is painful enough that users never finish initial setup | Provide CSV import for initial collection population. Consider a "quick add" mode with minimal fields. |
| Thread resolution is destructive | User resolves a thread and loses all the research notes and rejected candidates | Archive resolved threads, do not delete them. Users want to reference why they chose item X over Y months later. |
| Flat item list with no visual grouping | Collection becomes an unscannable wall of text at 50+ items | Group by tag/category in the default view. Provide sort options (weight, price, date added). Show item thumbnails in list view. |
| Weight displayed without context | "450g" means nothing without knowing if that is heavy or light for this category | Show weight relative to the lightest/heaviest item in the same category, or relative to the item being replaced |
| No "undo" for destructive actions | Accidental deletion of an item with detailed notes is unrecoverable | Soft-delete with a 30-day trash, or at minimum a confirmation dialog that names the item being deleted |
## "Looks Done But Isn't" Checklist
Things that appear complete but are missing critical pieces.
- [ ] **Item CRUD:** Often missing image cleanup on delete -- verify orphaned images are removed when items are deleted
- [ ] **Planning threads:** Often missing the "link to existing collection item being replaced" -- verify threads can reference what they are upgrading
- [ ] **Setup composition:** Often missing recomputation on item changes -- verify that editing an item's weight updates all setups containing it
- [ ] **CSV import:** Often missing unit detection/conversion -- verify that importing "5 oz" vs "142g" both result in correct canonical storage
- [ ] **Thread resolution:** Often missing setup propagation -- verify that resolving a thread and adding the winner to collection offers to update setups that contained the replaced item
- [ ] **Comparison view:** Often missing delta computation -- verify that the comparison shows differences between candidates, not just raw values side by side
- [ ] **Dashboard totals:** Often missing staleness handling -- verify dashboard stats reflect current data, not cached snapshots
- [ ] **Item deletion:** Often missing setup impact check -- verify the user is warned "This item is in 3 setups" before confirming deletion
## Recovery Strategies
When pitfalls occur despite prevention, how to recover.
| Pitfall | Recovery Cost | Recovery Steps |
|---------|---------------|----------------|
| Mixed units without conversion | MEDIUM | Add unit column to items table. Write a migration script that prompts user to confirm/correct units for existing items. Recompute all setup totals. |
| Rigid category hierarchy | HIGH | Migrate categories to tags (each leaf category becomes a tag). Update all item references. Redesign category UI to tag-based UI. |
| Thread state machine bugs | MEDIUM | Audit all threads for impossible states. Write a cleanup script. Add transition validation. Retest all state transitions. |
| Image path breakage | LOW-MEDIUM | Write a script that scans DB for broken image paths. Move images to canonical location. Update paths. Add fallback placeholder. |
| Stale setup totals | LOW | Drop cached total columns. Replace with computed queries. One-time migration, no data loss. |
| Currency as float | MEDIUM | Multiply all price values by 100, change column type to integer (cents). Rounding during conversion may lose sub-cent precision. |
## Pitfall-to-Phase Mapping
How roadmap phases should address these pitfalls.
| Pitfall | Prevention Phase | Verification |
|---------|------------------|--------------|
| Unit handling | Phase 1: Data model | Schema stores canonical grams + original unit. Conversion utility exists with tests. |
| Category rigidity | Phase 1: Data model | Items have a tags array/join table. No hierarchical category table exists. |
| Image storage | Phase 1: Core CRUD | Images stored in `data/images/` with relative paths. Thumbnails generated on upload. Cleanup on delete. |
| Currency precision | Phase 1: Data model | Price stored as integer cents. Display layer formats to dollars/euros. |
| Thread state machine | Phase 2: Planning threads | State transitions documented in code. Invalid transitions throw errors. Resolution is transactional. |
| Comparison usefulness | Phase 2: Planning threads | Comparison view shows deltas. Thread can link to "item being replaced." Setup impact visible. |
| Setup integrity | Phase 3: Setups | Totals computed from live data. Item deletion warns about setup membership. Soft-delete or archive for removed items. |
| Data loss / no backup | Phase 1: Infrastructure | Automatic DB backup on a schedule. Manual export button on dashboard. |
| Bulk import | Phase 1: Core CRUD | CSV import available from collection view. Handles unit variations in weight column. |
## Sources
- [Ultralight: The Gear Tracking App I'm Leaving LighterPack For](https://trailsmag.net/blogs/hiker-box/ultralight-the-gear-tracking-app-i-m-leaving-lighterpack-for) -- LighterPack limitations and community complaints
- [SQLite Internal vs External BLOBs](https://sqlite.org/intern-v-extern-blob.html) -- official SQLite guidance on image storage tradeoffs
- [35% Faster Than The Filesystem](https://sqlite.org/fasterthanfs.html) -- SQLite BLOB performance data
- [Comparison Tables for Products, Services, and Features - NN/g](https://www.nngroup.com/articles/comparison-tables/) -- comparison UX best practices
- [Designing The Perfect Feature Comparison Table - Smashing Magazine](https://www.smashingmagazine.com/2017/08/designing-perfect-feature-comparison-table/) -- comparison table design patterns
- [Comparing products: UX design best practices - Contentsquare](https://contentsquare.com/blog/comparing-products-design-practices-to-help-your-users-avoid-fragmented-comparison-7/) -- product comparison UX pitfalls
- [Common Unit Conversion Mistakes That Break Applications](https://helppdev.com/en/blog/common-unit-conversion-mistakes-that-break-applications) -- unit conversion antipatterns
- [Inventory App Design - UXPin](https://www.uxpin.com/studio/blog/inventory-app-design/) -- inventory app UX patterns
- [Designing better file organization around tags, not hierarchies](https://www.nayuki.io/page/designing-better-file-organization-around-tags-not-hierarchies) -- tags vs hierarchy tradeoffs
---
*Pitfalls research for: GearBox -- gear management and purchase planning app*
*Researched: 2026-03-14*

191
.planning/research/STACK.md Normal file
View File

@@ -0,0 +1,191 @@
# Stack Research
**Domain:** Single-user gear management and purchase planning web app
**Researched:** 2026-03-14
**Confidence:** HIGH
## Recommended Stack
### Core Technologies
| Technology | Version | Purpose | Why Recommended |
|------------|---------|---------|-----------------|
| Bun | 1.3.x | Runtime, package manager, bundler | User constraint. Built-in SQLite, fast installs, native TS support. Eliminates need for separate runtime/bundler/pkg manager. |
| React | 19.2.x | UI framework | Industry standard, massive ecosystem, stable. Server Components not needed for this SPA -- stick with client-side React. |
| Vite | 8.0.x | Dev server, production builds | Rolldown-based builds (5-30x faster than Vite 7). Zero-config React support. Bun-compatible. HMR out of the box. |
| Hono | 4.12.x | Backend API framework | Built on Web Standards, first-class Bun support, zero dependencies, tiny (~12kB). Perfect for a lightweight REST API. Faster than Express on Bun benchmarks. |
| SQLite (bun:sqlite) | Built-in | Database | Zero-dependency, built into Bun runtime. 3-6x faster than better-sqlite3. Single file database -- perfect for single-user app. No server process to manage. |
| Drizzle ORM | 0.45.x | Database ORM, migrations | Type-safe SQL, ~7.4kB, zero dependencies. Native bun:sqlite driver support. SQL-like query API (not abstracting SQL away). Built-in migration tooling via drizzle-kit. |
| Tailwind CSS | 4.2.x | Styling | CSS-native configuration (no JS config file). Auto content detection. Microsecond incremental builds. Perfect for "light, airy, minimalist" design constraint. |
| TanStack Router | 1.167.x | Client-side routing | Full type-safe routing with typed params and search params. File-based route generation. Better SPA experience than React Router v7 (whose best features require framework mode). |
| TanStack Query | 5.93.x | Server state management | Handles API data fetching, caching, and synchronization. Eliminates manual loading/error state management. Automatic cache invalidation on mutations. |
| Zustand | 5.0.x | Client state management | Minimal boilerplate, ~1kB. For UI state like active filters, modal state, theme. TanStack Query handles server state; Zustand handles the rest. |
| Zod | 4.3.x | Schema validation | Validates API inputs on the server, form data on the client, and shares types between both. Single source of truth for data shapes. |
| TypeScript | 5.x (Bun built-in) | Type safety | Bun transpiles TS natively -- no tsc needed at runtime. Catches bugs at dev time. Required by Drizzle and TanStack Router for type-safe queries and routes. |
### Supporting Libraries
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| @tanstack/react-query-devtools | 5.x | Query debugging | Development only. Inspect cache state, refetch timing, query status. |
| drizzle-kit | latest | DB migrations CLI | Run `drizzle-kit generate` and `drizzle-kit migrate` for schema changes. |
| @hono/zod-validator | latest | Request validation middleware | Validate API request bodies/params using Zod schemas in Hono routes. |
| clsx | 2.x | Conditional class names | When building components with variant styles. Pairs with Tailwind. |
| @tanstack/react-router-devtools | latest | Router debugging | Development only. Inspect route matches, params, search params. |
### Development Tools
| Tool | Purpose | Notes |
|------|---------|-------|
| Bun | Test runner | `bun test` -- built-in, Jest-compatible API. No need for Vitest or Jest. |
| Biome | Linter + formatter | Single tool replacing ESLint + Prettier. Fast (Rust-based), minimal config. `biome check --write` does both. |
| Vite React plugin | React HMR/JSX | `@vitejs/plugin-react` for Fast Refresh during development. |
## Installation
```bash
# Initialize project
bun init
# Core frontend
bun add react react-dom @tanstack/react-router @tanstack/react-query zustand zod clsx
# Core backend
bun add hono @hono/zod-validator drizzle-orm
# Styling
bun add tailwindcss @tailwindcss/vite
# Build tooling
bun add -d vite @vitejs/plugin-react typescript @types/react @types/react-dom
# Database tooling
bun add -d drizzle-kit
# Linting + formatting
bun add -d @biomejs/biome
# Dev tools (optional but recommended)
bun add -d @tanstack/react-query-devtools @tanstack/react-router-devtools
```
## Architecture Pattern
**Monorepo-lite (single package, split directories):**
```
/src
/client -- React SPA (Vite entry point)
/routes -- TanStack Router file-based routes
/components -- Shared UI components
/stores -- Zustand stores
/api -- TanStack Query hooks (fetch wrappers)
/server -- Hono API server
/routes -- API route handlers
/db -- Drizzle schema, migrations
/shared -- Zod schemas shared between client and server
/public -- Static assets, uploaded images
```
Bun runs the Hono server, which also serves the Vite-built SPA in production. In development, Vite dev server proxies API calls to the Hono backend.
## Alternatives Considered
| Recommended | Alternative | When to Use Alternative |
|-------------|-------------|-------------------------|
| Hono | Elysia | If you want end-to-end type safety with Eden Treaty. Elysia is Bun-native but heavier, more opinionated, and has a smaller ecosystem than Hono. |
| Hono | Express | Never for new Bun projects. Express is Node-centric, not built on Web Standards, slower on Bun. |
| TanStack Router | React Router v7 | If you want the simplest possible routing with minimal type safety. React Router v7's best features (loaders, type safety) require framework mode which adds complexity. |
| Drizzle ORM | Prisma | If you have a complex relational model and want auto-generated migrations. But Prisma is heavy (~8MB), generates a query engine binary, and has weaker SQLite support. |
| Drizzle ORM | Kysely | If you want a pure query builder without ORM features. Kysely is lighter but lacks built-in migration tooling. |
| Zustand | Jotai | If you prefer atomic state (bottom-up). Zustand is simpler for this app's needs -- a few global stores, not many independent atoms. |
| Tailwind CSS | Vanilla CSS / CSS Modules | If you strongly prefer writing plain CSS. But Tailwind accelerates building consistent minimalist UIs and requires less design system setup. |
| bun:sqlite | PostgreSQL | If you later need multi-user with concurrent writes. Overkill for single-user. Adds a database server dependency. |
| Biome | ESLint + Prettier | If you need specific ESLint plugins not yet in Biome. But Biome covers 95% of use cases with zero config. |
| Vite | Bun's built-in bundler | Bun can serve HTML directly as of 1.3, but Vite's ecosystem (plugins, HMR, proxy) is far more mature for SPA development. |
## What NOT to Use
| Avoid | Why | Use Instead |
|-------|-----|-------------|
| Next.js | Server-centric framework. Massive overhead for a single-user SPA. Forces Node.js patterns. No benefit without SSR/SSG needs. | Vite + React + Hono |
| Remix / React Router framework mode | Adds server framework complexity. This is a simple SPA with a separate API -- framework routing is unnecessary overhead. | TanStack Router (SPA mode) |
| better-sqlite3 | Requires native compilation, compatibility issues with Bun. bun:sqlite is built-in and 3-6x faster. | bun:sqlite (built into Bun) |
| Redux / Redux Toolkit | Massive boilerplate for a small app. Actions, reducers, slices -- all unnecessary when Zustand does the same in 10 lines. | Zustand |
| Mongoose / MongoDB | Document DB is wrong fit. Gear items have relational structure (items belong to setups, threads reference items). SQL is the right model. | Drizzle + SQLite |
| Axios | Unnecessary abstraction over fetch. Bun and browsers both have native fetch. TanStack Query wraps fetch already. | Native fetch |
| styled-components / Emotion | CSS-in-JS adds runtime overhead and bundle size. Tailwind is faster (zero runtime) and better for consistent minimalist design. | Tailwind CSS |
| Jest / Vitest | Bun has a built-in test runner with Jest-compatible API. No need for external test frameworks. | bun test |
| ESLint + Prettier | Two tools, complex configuration, slow (JS-based). Biome does both in one tool, faster. | Biome |
## Version Compatibility
| Package A | Compatible With | Notes |
|-----------|-----------------|-------|
| Bun 1.3.x | bun:sqlite (built-in) | SQLite driver is part of the runtime, always compatible. |
| Drizzle ORM 0.45.x | bun:sqlite via `drizzle-orm/bun-sqlite` | Official driver. Import from `drizzle-orm/bun-sqlite`. |
| Drizzle ORM 0.45.x | drizzle-kit (latest) | drizzle-kit handles migration generation/execution. Must match major drizzle-orm version. |
| React 19.2.x | TanStack Router 1.x | TanStack Router 1.x supports React 18+ and 19.x. |
| React 19.2.x | TanStack Query 5.x | TanStack Query 5.x supports React 18+ and 19.x. |
| React 19.2.x | Zustand 5.x | Zustand 5.x supports React 18+ and 19.x. |
| Vite 8.x | @vitejs/plugin-react | Check plugin version matches Vite major. Use latest plugin for Vite 8. |
| Tailwind CSS 4.2.x | @tailwindcss/vite | v4 uses Vite plugin instead of PostCSS. Import as `@tailwindcss/vite` in vite config. |
| Zod 4.x | @hono/zod-validator | Verify @hono/zod-validator supports Zod 4. If not, pin Zod 3.23.x until updated. |
## Key Configuration Notes
### Bun + Vite Setup
Vite runs as the dev server for the frontend. The Hono API server runs separately. Use Vite's `server.proxy` to forward `/api/*` requests to the Hono backend during development.
### SQLite WAL Mode
Enable WAL mode on database initialization for better performance:
```typescript
import { Database } from "bun:sqlite";
const db = new Database("gearbox.db");
db.run("PRAGMA journal_mode = WAL");
db.run("PRAGMA foreign_keys = ON");
```
### Tailwind v4 (No Config File)
Tailwind v4 uses CSS-native configuration. No `tailwind.config.js` needed:
```css
@import "tailwindcss";
@theme {
--color-primary: #2563eb;
--font-sans: "Inter", sans-serif;
}
```
### Drizzle Schema Example (bun:sqlite)
```typescript
import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core";
export const gearItems = sqliteTable("gear_items", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
category: text("category").notNull(),
weightGrams: real("weight_grams"),
priceCents: integer("price_cents"),
source: text("source"),
notes: text("notes"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
});
```
## Sources
- [Bun official docs](https://bun.com/docs) -- bun:sqlite features, runtime capabilities (HIGH confidence)
- [Hono official docs](https://hono.dev/docs) -- Bun integration, static serving (HIGH confidence)
- [Drizzle ORM docs - Bun SQLite](https://orm.drizzle.team/docs/connect-bun-sqlite) -- driver support verified (HIGH confidence)
- [Vite releases](https://vite.dev/releases) -- v8.0 with Rolldown confirmed (HIGH confidence)
- [Tailwind CSS v4.2 blog](https://tailwindcss.com/blog/tailwindcss-v4) -- CSS-native config, Vite plugin (HIGH confidence)
- [TanStack Router docs](https://tanstack.com/router/latest) -- v1.167.x confirmed (HIGH confidence)
- [TanStack Query docs](https://tanstack.com/query/latest) -- v5.93.x for React (HIGH confidence)
- [Zustand npm](https://www.npmjs.com/package/zustand) -- v5.0.x confirmed (HIGH confidence)
- [Zod v4 release notes](https://zod.dev/v4) -- v4.3.x confirmed (MEDIUM confidence -- verify @hono/zod-validator compatibility)
- [React versions](https://react.dev/versions) -- v19.2.x confirmed (HIGH confidence)
- [Bun SQLite vs better-sqlite3 benchmarks](https://bun.com/docs/runtime/sqlite) -- 3-6x performance advantage (HIGH confidence)
---
*Stack research for: GearBox -- gear management and purchase planning web app*
*Researched: 2026-03-14*

View File

@@ -0,0 +1,243 @@
# Project Research Summary
**Project:** GearBox
**Domain:** Single-user gear management and purchase planning web app
**Researched:** 2026-03-14
**Confidence:** HIGH
## Executive Summary
GearBox is a single-user personal gear management app with a critical differentiator: purchase planning threads. Every competitor (LighterPack, GearGrams, Packstack, Hikt) is a post-purchase inventory tool — they help you track what you own. GearBox closes the loop by adding a structured pre-purchase research workflow where users compare candidates, track research status, and resolve threads by promoting winners into their collection. This is the entire reason to build the product; the collection management side is table stakes, and the purchase planning threads are the moat. Research strongly recommends building both together in the v1 scope, not sequencing them separately, because the thread resolution workflow only becomes compelling once a real collection exists to reference.
The recommended architecture is a single-process Bun fullstack monolith: Hono for the API layer, React 19 + Vite 8 for the frontend, Drizzle ORM + bun:sqlite for the database, TanStack Router + TanStack Query for client navigation and server state, and Tailwind CSS v4 for styling. This stack is purpose-built for the constraints: Bun is a project requirement, SQLite is optimal for single-user, and every tool in the list has zero or near-zero runtime overhead. Zustand handles the small amount of client-only UI state. The entire stack is type-safe end-to-end through Zod schemas shared between client and server.
The biggest risks are front-loaded in Phase 1: unit handling (weights must be canonicalized to grams from day one), currency precision (prices must be stored as integer cents), category flexibility (must use user-defined tags, not a hardcoded hierarchy), and image storage strategy (relative paths to a local directory, never BLOBs for full-size, never absolute paths). Getting these wrong requires painful data migrations later. The second major risk is the thread state machine in Phase 2 — the combination of candidate status, thread lifecycle, and "move winner to collection" creates a stateful flow that must be modeled as an explicit state machine with transactional resolution, not assembled incrementally.
## Key Findings
### Recommended Stack
The stack is a tightly integrated Bun-native toolchain with no redundant tools. Bun serves as runtime, package manager, test runner, and provides built-in SQLite — eliminating entire categories of infrastructure. Vite 8 (Rolldown-based, 5-30x faster than Vite 7) handles the dev server and production frontend builds. The client-server boundary is clean: Hono serves the API, React handles the UI, and Zod schemas in a `shared/` directory provide a single source of truth for data shapes on both sides.
The architecture note in STACK.md suggests Bun's fullstack HTML-based routing (not Vite's dev server proxy pattern). This differs slightly from the standard Vite proxy setup: each page is a separate HTML entrypoint imported into `Bun.serve()`, and TanStack Router handles in-page client-side navigation only. This simplifies the development setup to a single `bun run` command with no proxy configuration.
**Core technologies:**
- Bun 1.3.x: Runtime, package manager, test runner, bundler — eliminates Node.js and npm
- React 19.2.x + Vite 8.x: SPA framework + dev server — stable, large ecosystem, HMR out of the box
- Hono 4.12.x: API layer — Web Standards based, first-class Bun support, ~12kB, faster than Express on Bun
- SQLite (bun:sqlite) + Drizzle ORM 0.45.x: Database — zero-dependency, built into Bun, type-safe queries and migrations
- TanStack Router 1.167.x + TanStack Query 5.93.x: Routing + server state — full type-safe routing, automatic cache invalidation
- Tailwind CSS 4.2.x: Styling — CSS-native config, no JS file, microsecond incremental builds
- Zustand 5.x: Client UI state — minimal boilerplate for filter state, modals, theme
- Zod 4.3.x: Schema validation — shared between client and server as single source of truth for types
- Biome: Linting + formatting — replaces ESLint + Prettier, Rust-based, near-zero config
**Version flag:** Verify that `@hono/zod-validator` supports Zod 4.x before starting. If not, pin Zod 3.23.x until the validator is updated.
### Expected Features
The feature research distinguishes cleanly between what every gear app does (table stakes) and what GearBox uniquely does (purchase planning threads). No competitor has threads, candidate comparison, or thread resolution. This is the entire competitive surface. Everything else is hygiene.
**Must have (table stakes) — v1 launch:**
- Item CRUD with weight, price, category, notes, product URL — minimum unit of value
- User-defined categories/tags — must be flexible, not a hardcoded hierarchy
- Weight unit support (g, oz, lb, kg) — gear community requires this; store canonical grams internally
- Automatic weight/cost totals by category and setup — the reason to use an app over a text file
- Named setups composed from collection items — compose loadouts, get aggregate totals
- Planning threads with candidate items — the core differentiator
- Side-by-side candidate comparison with deltas (not just raw values) — the payoff of threads
- Thread resolution: pick winner, move to collection — closes the purchase research loop
- Search and filter on collection — essential at 30+ items
- Dashboard home page — clean entry point per project constraints
**Should have (competitive) — v1.x after validation:**
- Impact preview: how a thread candidate changes a specific setup's weight and cost
- Status tracking on thread items (researching / ordered / arrived)
- Priority/ranking within threads
- Photos per item (one photo per item initially)
- CSV import/export — migration path from spreadsheets, data portability
- Weight distribution visualization (pie/bar chart by category)
**Defer — v2+:**
- Multi-photo gallery per item
- Shareable read-only links for setups
- Drag-and-drop reordering
- Bulk operations (multi-select, bulk delete)
- Dark mode
- Item history/changelog
### Architecture Approach
The architecture is a monolithic Bun process with a clear 4-layer structure: API routes (HTTP concerns), service layer (business logic and calculations), Drizzle ORM (type-safe data access), and bun:sqlite (embedded storage). There are no microservices, no Docker, no external database server. The client is a React SPA served as static files by the same Bun process. Internal communication is REST + JSON; no WebSockets needed. The data model has three primary entities — items, threads (with candidates), and setups — connected by explicit foreign keys and a junction table for the many-to-many setup-to-items relationship.
**Major components:**
1. Collection (items): Core entity. Source of truth for owned gear. Every other feature references items.
2. Planning Threads (threads + candidates): Pre-purchase research. Thread lifecycle is a state machine; resolution is transactional.
3. Setups: Named loadouts composed from collection items. Totals are always computed live from item data, never cached.
4. Service Layer: Business logic isolated from HTTP concerns. Enables testing without HTTP mocking. Key: `calculateSetupTotals()`, `computeCandidateImpact()`.
5. Dashboard: Read-only aggregation. Built last since it reads from all other entities.
6. Image Storage: Filesystem (`./uploads/` or `data/images/{item-id}/`) with relative paths in DB. Thumbnails on upload.
**Build order from ARCHITECTURE.md (follow this):**
1. Database schema (Drizzle) — everything depends on this
2. Items API (CRUD) — the core entity
3. Collection UI — first visible feature, validates end-to-end
4. Threads + candidates API and UI — depends on items for resolution
5. Setups API and UI — depends on items for composition
6. Dashboard — aggregates from all entities, build last
7. Polish: image upload, impact calculations, status tracking
### Critical Pitfalls
1. **Unit handling treated as display-only** — Store all weights as canonical grams at write time. Accept any unit as input, convert on save. Build a `weightToGrams(value, unit)` utility on day one. A bare number field with no unit tracking will silently corrupt all aggregates when users paste specs in mixed units.
2. **Rigid category hierarchy** — Use user-defined flat tags, not a hardcoded category tree. A `categories` table with `parent_id` foreign keys will fail the moment a user tries to track sim racing gear or photography equipment. Tags allow many-to-many, support any hobby, and do not require schema changes to add a new domain.
3. **Thread state machine complexity** — Model the thread lifecycle as an explicit state machine before writing any code. Document valid transitions. The "resolve thread" action must be a single atomic transaction: validate winner exists, create collection item, mark thread resolved, update candidate statuses. Without this, impossible states (resolved thread with active candidates, ghost items in collection) accumulate silently.
4. **Setup totals cached in the database** — Never store `totalWeight` or `totalCost` on a setup record. Always compute from live item data via `SUM()`. Cached totals go stale the moment any member item is edited, and the bugs are subtle (the UI shows a total that doesn't match the items).
5. **Comparison view that displays data but doesn't aid decisions** — The comparison view must show deltas between candidates and against the item being replaced from the collection, not just raw values side by side. Color-code lighter/heavier, cheaper/more expensive. A comparison table with no computed differences is worse than a spreadsheet.
**Additional high-priority pitfalls to address per phase:**
- Currency stored as floats (use integer cents always)
- Image paths stored as absolute paths or as BLOBs for full-size images
- Thread resolution is destructive (archive threads, don't delete them — users need to reference why they chose X over Y)
- Item deletion without setup impact warning
## Implications for Roadmap
Based on the combined research, a 5-phase structure is recommended. Phases 1-3 deliver the v1 MVP; Phases 4-5 deliver the v1.x feature set.
### Phase 1: Foundation — Data Model, Infrastructure, Core Item CRUD
**Rationale:** Everything depends on getting the data model right. Unit handling, currency precision, category flexibility, image storage strategy, and the items schema are all Phase 1 decisions. Getting these wrong requires expensive data migrations. The architecture research explicitly states: "Database schema + Drizzle setup — Everything depends on the data model." The pitfalls research agrees: 6 of 9 pitfalls have "Phase 1" as their prevention phase.
**Delivers:** Working gear catalog — users can add, edit, delete, and browse their collection. Item CRUD with all core fields. Weight unit conversion. User-defined categories. Image upload with thumbnail generation and cleanup on delete. SQLite database with WAL mode enabled, automatic backup mechanism, and all schemas finalized.
**Features from FEATURES.md:** Item CRUD with core fields, user-defined categories, weight unit support (g/oz/lb/kg), notes and product URL fields, search and filter.
**Pitfalls to prevent:** Unit handling (canonical grams), currency precision (integer cents), category flexibility (user-defined tags, no hierarchy), image storage (relative paths, thumbnails), data loss prevention (WAL mode, auto-backup mechanism).
**Research flag:** Standard patterns. Schema design for inventory apps is well-documented. No research phase needed.
---
### Phase 2: Planning Threads — The Core Differentiator
**Rationale:** Threads are why GearBox exists. The feature dependency graph in FEATURES.md shows threads require items to exist (to resolve candidates into the collection), which is why Phase 1 must complete first. The thread state machine is the most complex feature in the product and gets its own phase to ensure the state transitions are modeled correctly before any UI is built.
**Delivers:** Complete purchase planning workflow — create threads, add candidates with weight/price/notes, compare candidates side-by-side with weight/cost deltas (not just raw values), resolve threads by selecting a winner and moving it to the collection, archive resolved threads.
**Features from FEATURES.md:** Planning threads, side-by-side candidate comparison (with deltas), thread resolution workflow. Does not include status tracking (researching/ordered/arrived) or priority/ranking — those are v1.x.
**Pitfalls to prevent:** Thread state machine complexity (model transitions explicitly, transactional resolution), comparison usefulness (show deltas and impact, not just raw data), thread archiving (never destructive resolution).
**Research flag:** Needs careful design work before coding. The state machine for thread lifecycle (open -> in-progress -> resolved/cancelled) combined with candidate status (researching / ordered / arrived) and the resolution side-effect (create collection item) has no off-the-shelf reference implementation. Design the state diagram first.
---
### Phase 3: Setups — Named Loadouts and Composition
**Rationale:** Setups require items to exist (Phase 1) and benefit from threads being stable (Phase 2) because thread resolution can affect setup membership (the replaced item should be updatable in setups). The many-to-many setup-items relationship and the setup integrity pitfall require careful foreign key design.
**Delivers:** Named setups composed from collection items. Weight and cost totals computed live (never cached). Base/worn/consumable weight classification per item per setup. Category weight breakdown. Item deletion warns about setup membership. Visual indicator when a setup item is no longer in the collection.
**Features from FEATURES.md:** Named setups with item selection and totals, setup weight/cost breakdown by category, automatic totals.
**Pitfalls to prevent:** Setup totals cached in DB (always compute live), setup composition breaks on collection changes (explicit `ON DELETE` behavior, visual indicators for missing items, no silent CASCADE).
**Research flag:** Standard patterns for junction table composition. No research phase needed for the setup-items relationship. The weight classification (base/worn/consumable) per setup entry is worth a design session — this is per-setup metadata on the junction, not a property of the item itself.
---
### Phase 4: Dashboard and Polish
**Rationale:** The architecture research explicitly states "Dashboard — aggregates stats from all other entities. Build last since it reads from everything." Dashboard requires all prior phases to be stable since it reads from items, threads, and setups simultaneously. This phase also adds the weight visualization chart that requires a full dataset to be meaningful.
**Delivers:** Dashboard home page with summary cards (item count, active threads, setup count, collection value). Weight distribution visualization (pie/bar chart by category). Dashboard stats endpoint (`/api/stats`) as a read-only aggregation. General UI polish for the "light, airy, minimalist" aesthetic.
**Features from FEATURES.md:** Dashboard home page, weight distribution visualization.
**Research flag:** Standard patterns. Dashboard aggregation is a straightforward read-only endpoint. Charting is well-documented. No research phase needed.
---
### Phase 5: v1.x Enhancements
**Rationale:** These features add significant value but depend on the core (Phases 1-3) being proven out. Impact preview requires both stable setups and stable threads. CSV import/export validates the data model is clean (if import is buggy, the model has problems). Photos add storage complexity that is easier to handle once the core CRUD flow is solid.
**Delivers:** Impact preview (how a thread candidate changes a specific setup's weight/cost). Thread item status tracking (researching / ordered / arrived). Priority/ranking within threads. Photos per item (upload, display, cleanup). CSV import/export with unit detection.
**Features from FEATURES.md:** Impact preview, status tracking, priority/ranking, photos per item, CSV import/export.
**Pitfalls to prevent:** CSV import missing unit conversion (must detect and convert oz/lb/kg to grams on import). Image uploads without size/type validation. Product URLs not sanitized (validate http/https protocol, render with `rel="noopener noreferrer"`).
**Research flag:** CSV import with unit detection may need a design pass — handling "5 oz", "142g", "0.3 lb" in the same weight column requires a parsing strategy. Worth a short research spike before implementation.
---
### Phase Ordering Rationale
- **Data model first:** Six of nine pitfalls identified are Phase 1 prevention items. The schema is the hardest thing to change later and the most consequential.
- **Threads before setups:** Thread resolution creates collection items; setup composition consumes them. But more importantly, threads are the differentiating feature — proving the thread workflow works is more valuable than setups.
- **Dashboard last:** Explicitly recommended by architecture research. Aggregating from incomplete entities produces misleading data and masks bugs.
- **Impact preview in Phase 5:** This feature requires both stable setups (Phase 3) and stable threads (Phase 2). Building it before both are solid means rebuilding it when either changes.
- **Photos deferred to Phase 5:** The core value proposition is weight/cost tracking and purchase planning, not a photo gallery. Adding photo infrastructure in Phase 1 increases scope without validating the core concept.
### Research Flags
**Needs design/research before coding:**
- **Phase 2 (Thread State Machine):** Design the state diagram for thread lifecycle x candidate status before writing any code. Define all valid transitions and invalid states explicitly. This is the most stateful feature in the product and has no off-the-shelf pattern to follow.
- **Phase 5 (CSV Import):** Design the column-mapping and unit-detection strategy before implementation. The spreadsheet-to-app migration workflow is critical for the target audience (users migrating from gear spreadsheets).
**Standard patterns — no research phase needed:**
- **Phase 1 (Data model + CRUD):** Schema design for inventory apps is well-documented. Drizzle + bun:sqlite patterns are covered in official docs.
- **Phase 3 (Setups):** Junction table composition is a standard relational pattern. Foreign key behavior for integrity is documented.
- **Phase 4 (Dashboard):** Aggregation endpoints and charting are standard. No novel patterns.
## Confidence Assessment
| Area | Confidence | Notes |
|------|------------|-------|
| Stack | HIGH | All technologies verified against official docs. Version compatibility confirmed. One flag: verify `@hono/zod-validator` supports Zod 4.x before starting. |
| Features | HIGH | Competitor analysis is thorough (LighterPack, GearGrams, Packstack, Hikt all compared). Feature gaps and differentiators are clearly identified. |
| Architecture | HIGH | Bun fullstack monolith pattern is official and well-documented. Service layer and data flow patterns are standard. |
| Pitfalls | HIGH | Pitfalls are domain-specific and well-sourced. SQLite BLOB guidance from official SQLite docs. Comparison UX from NN/g. Unit conversion antipatterns documented. |
**Overall confidence: HIGH**
### Gaps to Address
- **Zod 4 / @hono/zod-validator compatibility:** STACK.md flags this explicitly. Verify before starting. If incompatible, pin Zod 3.23.x. This is a quick check, not a blocker.
- **Bun fullstack vs. Vite proxy setup:** STACK.md describes the Vite dev server proxy pattern (standard approach), while ARCHITECTURE.md describes Bun's HTML-based routing with `Bun.serve()` (newer approach). These are two valid patterns. The architecture file's approach (Bun fullstack) is simpler for production deployment. Confirm which pattern to follow before project setup — they require different `vite.config.ts` and entry point structures.
- **Weight classification (base/worn/consumable) data model:** Where does this live? On the `setup_items` junction table (per-setup classification, same item can be "base" in one setup and "worn" in another) or on the item itself (one classification for all setups)? The per-setup model is more flexible but more complex. Decide in Phase 1 schema design, not Phase 3 when setups are built.
- **Tag vs. single-category field:** PITFALLS.md recommends a flat tag system. FEATURES.md implies a single "category" field. The right answer is probably a single optional category field (for broad grouping, e.g., "clothing") plus user-defined tags for fine-grained organization. Confirm the data model in Phase 1.
## Sources
### Primary (HIGH confidence)
- [Bun official docs](https://bun.com/docs) — bun:sqlite, fullstack dev server, Bun.serve() routing
- [Hono official docs](https://hono.dev/docs) — Bun integration, middleware patterns
- [Drizzle ORM docs - Bun SQLite](https://orm.drizzle.team/docs/connect-bun-sqlite) — driver support, schema patterns
- [Vite releases](https://vite.dev/releases) — v8.0 with Rolldown confirmed
- [Tailwind CSS v4.2 blog](https://tailwindcss.com/blog/tailwindcss-v4) — CSS-native config, Vite plugin
- [TanStack Router docs](https://tanstack.com/router/latest) — file-based routing, typed params
- [TanStack Query docs](https://tanstack.com/query/latest) — cache invalidation, mutations
- [SQLite Internal vs External BLOBs](https://sqlite.org/intern-v-extern-blob.html) — image storage guidance
- [Comparison Tables — NN/g](https://www.nngroup.com/articles/comparison-tables/) — comparison UX best practices
### Secondary (MEDIUM confidence)
- [Hikt Blog: Best Backpacking Gear Apps 2026](https://hikt.app/blog/best-backpacking-gear-apps-2026/) — competitor feature analysis
- [Building Full-Stack App with Bun.js, React and Drizzle ORM](https://awplife.com/building-full-stack-app-with-bun-js-react-drizzle/) — project structure reference
- [Designing better file organization around tags, not hierarchies](https://www.nayuki.io/page/designing-better-file-organization-around-tags-not-hierarchies) — tags vs hierarchy rationale
### Tertiary (LOW confidence / needs validation)
- [Zod v4 release notes](https://zod.dev/v4) — @hono/zod-validator compatibility with Zod 4 unconfirmed, verify before use
---
*Research completed: 2026-03-14*
*Ready for roadmap: yes*