# 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 `` 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*