Trigger on `push: tags` instead of `workflow_dispatch`. The pushed tag name is the version — removes the bump-input + compute-and-create-tag dance, avoiding accidental major bumps from the manual dispatch form. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.7 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
GearBox is a single-user web app for managing gear collections (bikepacking, sim racing, etc.), tracking weight/price, and planning purchases through research threads. Full-stack TypeScript monolith running on Bun.
Commands
# Development
bun run dev # Starts both Vite client (:5173) and Hono server (:3000) concurrently
bun run dev:client # Vite dev server on :5173 (proxies /api to :3000)
bun run dev:server # Hono server on :3000 with hot reload
# Database
bun run db:generate # Generate Drizzle migration from schema changes
bun run db:push # Apply migrations to gearbox.db
# Testing
bun test # Run all unit/integration tests
bun test tests/services/item.service.test.ts # Run single test file
bun run test:e2e # Run Playwright E2E tests
bun run test:e2e:ui # Playwright UI mode for debugging
# Lint & Format
bun run lint # Biome check (tabs, double quotes, organized imports)
# Build
bun run build # Vite build → dist/client/
Architecture
Stack: React 19 + Hono + Drizzle ORM + SQLite, all running on Bun.
Client (src/client/)
- Routing: TanStack Router with file-based routes in
src/client/routes/. Route tree auto-generated torouteTree.gen.ts— never edit manually. - Data fetching: TanStack React Query via custom hooks in
src/client/hooks/(e.g.,useItems,useThreads,useSetups). Mutations invalidate related query keys. - UI state: Zustand store (
stores/uiStore.ts) for panel/dialog state only — server data lives in React Query. - API calls: Thin fetch wrapper in
lib/api.ts(apiGet,apiPost,apiPut,apiDelete,apiUpload). - Styling: Tailwind CSS v4.
Server (src/server/)
- Routes (
routes/): Hono handlers with Zod validation via@hono/zod-validator. Delegate to services. - Services (
services/): Pure business logic functions that take a db instance. No HTTP awareness — testable without mocking. - Route registration in
src/server/index.tsviaapp.route("/api/...", routes).
Shared (src/shared/)
schemas.ts: Zod schemas for API request validation (source of truth for types).types.ts: Types inferred from Zod schemas + Drizzle table definitions. No manual type duplication.
Database (src/db/)
- Schema:
schema.ts— Drizzle table definitions for SQLite. - Prices stored as cents (
priceCents: integer) to avoid float rounding. - Timestamps: stored as integers (unix epoch) with
{ mode: "timestamp" }. - Tables:
categories,items,threads,threadCandidates,setups,setupItems,settings,users,sessions,apiKeys.
Testing (tests/ and e2e/)
- Unit/integration: Bun test runner (
bun test). Tests at service level and route level. tests/helpers/db.ts:createTestDb()creates in-memory SQLite via Drizzle migrations and seeds an "Uncategorized" category.- E2E: Playwright (
bun run test:e2e). Tests ine2e/run against a seeded SQLite database with the server in production mode. Seed script:e2e/seed.ts.
Releasing
Releases are tag-driven. The Gitea Actions workflow (.gitea/workflows/release.yml) triggers on any pushed tag matching v* — it runs CI (lint, test, build), generates a changelog from the previous tag, builds and pushes a Docker image, and creates a Gitea release. The version comes from the tag name.
To release, tag the desired commit on Develop and push:
git tag v2.3.0
git push origin v2.3.0
Branching
- Develop is the main branch. Keep it clean — don't commit large feature work directly.
- For each new brainstorming/implementation session, create a feature branch off Develop (e.g.,
feature/setup-impact-preview,fix/error-handling). - Merge back to Develop via PR or fast-forward merge when the work is complete and verified.
Path Alias
@/* maps to ./src/* (configured in tsconfig.json).
Reusable Components
Always use existing components instead of rebuilding with plain HTML. Check src/client/components/ before creating new form elements or UI patterns.
| Need | Use | Not |
|---|---|---|
| Category selection | CategoryPicker (icons, search, inline create) |
Plain <select> |
| Icon selection | IconPicker (119 curated Lucide icons, grouped) |
Manual icon input |
| Image upload | ImageUpload (preview, click-to-upload) |
Raw file input |
| Lucide icon rendering | LucideIcon from lib/iconData |
Direct SVG or lucide-react imports |
| Weight/price formatting | useFormatters() hook |
Manual formatting |
Key Patterns
- Thread resolution: Resolving a thread copies the winning candidate's data into a new item in the collection, sets
resolvedCandidateId, and changes status to "resolved". - Setup item sync:
PUT /api/setups/:id/itemsreplaces all setup_items atomically (delete all, re-insert). - Image uploads:
POST /api/imagessaves to S3-compatible storage (Garage/R2), returned asimageFilenameon item/candidate records. - Image URL fetching:
POST /api/images/from-urlfetches an image from a URL, saves to S3, returns{ filename, sourceUrl }. - Aggregates (weight/cost totals): Computed via SQL on read, not stored on records.
- Authentication: Public-read, authenticated-write. Cookie sessions for web UI, API keys (
X-API-Keyheader) for programmatic access.POST /api/auth/setupfor first-time account creation. Auth middleware protects all POST/PUT/DELETE on/api/*except/api/auth/*.
Authentication
- OIDC via Logto: Authentication is handled by an external Logto instance via
@hono/oidc-auth. Users are redirected to Logto for login, and sessions are managed via OIDC cookies. - Programmatic access: API keys created in Settings > API Keys. Pass via
X-API-Keyheader. - Public read: All GET endpoints work without auth. POST/PUT/DELETE require auth.
- Auth routes:
/api/auth/me,/api/auth/keys,/api/auth/profile. - MCP OAuth: OAuth 2.1 + PKCE for Claude mobile/web. Endpoints at
/oauth/*. Uses existing GearBox credentials.
Logto Setup
The Logto application must be configured with the correct scopes. In the Logto admin console, go to the application settings and ensure the following User Scopes are granted: openid, profile, email (matching the OIDC_SCOPES env var).
Required env vars:
OIDC_ISSUER=https://your-logto-domain/oidc # Logto OIDC issuer URL
OIDC_CLIENT_ID=<client-id> # From Logto app settings
OIDC_CLIENT_SECRET=<client-secret> # From Logto app settings
OIDC_AUTH_SECRET=<random-32-char-hex> # Session encryption key
OIDC_SCOPES="openid profile email" # Must match Logto app scopes
OIDC_REDIRECT_URI=https://your-app/callback # Must match Logto redirect URI
MCP Server
GearBox includes a built-in MCP server for integration with Claude Code and Claude Desktop. Enabled by default, disable with GEARBOX_MCP=false. Authenticated via API key or OAuth 2.1 Bearer token.
Tools (19 total)
| Tool | Description |
|---|---|
list_items |
List all items, optionally filter by category |
get_item |
Get item details by ID |
create_item |
Add item to collection (for decided items; use create_thread for research) |
update_item |
Update item details |
delete_item |
Remove item from collection |
list_categories |
List all categories |
create_category |
Create a new category |
list_threads |
List research threads (recommended workflow for gear purchases) |
get_thread |
Get thread with candidates |
create_thread |
Start a research thread to compare gear options |
resolve_thread |
Pick winning candidate, adds it to collection |
add_candidate |
Add candidate to a research thread |
update_candidate |
Update candidate details |
remove_candidate |
Remove candidate from thread |
list_setups |
List gear setups |
get_setup |
Get setup with items and totals |
create_setup |
Create a new setup |
update_setup |
Update setup name or items |
upload_image_from_url |
Fetch image from URL, save locally |
Resources
gearbox://collection/summary— Overview of collection: totals, items per category, active threads.
Configuration
Claude Code (.claude/settings.json):
{
"mcpServers": {
"gearbox": {
"type": "streamable-http",
"url": "http://localhost:3000/mcp",
"headers": {
"X-API-Key": "<your-api-key>"
}
}
}
}
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"gearbox": {
"type": "streamable-http",
"url": "http://localhost:3000/mcp",
"headers": {
"X-API-Key": "<your-api-key>"
}
}
}
}
Generate an API key from Settings > API Keys after logging in.
OAuth Authentication (Claude Mobile / claude.ai)
GearBox supports OAuth 2.1 Authorization Code + PKCE for MCP connections from Claude mobile app and claude.ai. The OAuth flow is automatic — Claude handles discovery and token exchange.
Required environment variable for production:
GEARBOX_URL=https://your-gearbox-domain.com # Used as OAuth issuer URL
OAuth endpoints:
GET /.well-known/oauth-authorization-server— Discovery metadataPOST /oauth/register— Dynamic Client RegistrationGET/POST /oauth/authorize— Authorization with login formPOST /oauth/token— Token exchange and refresh
Both auth methods work simultaneously:
- API key (
X-API-Keyheader) — Claude Code, scripts, programmatic access - OAuth Bearer token (
Authorization: Bearerheader) — Claude mobile, claude.ai