Files
GearBox/.planning/research/ARCHITECTURE.md

363 lines
21 KiB
Markdown

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