docs: complete project research
This commit is contained in:
@@ -1,260 +1,333 @@
|
||||
# Stack Research
|
||||
|
||||
**Domain:** Multi-user gear management platform (v2.0 platform additions)
|
||||
**Researched:** 2026-04-03
|
||||
**Confidence:** MEDIUM-HIGH
|
||||
**Domain:** Public-first gear discovery platform — catalog enrichment, discovery feed, agent-powered seeding (v2.1)
|
||||
**Researched:** 2026-04-09
|
||||
**Confidence:** HIGH (existing stack verified against package.json; additions verified against npm/official docs)
|
||||
|
||||
This document covers ONLY the new stack additions for v2.0. The existing stack (React 19, Hono, Drizzle ORM, TanStack Router/Query, Tailwind CSS v4, Lucide React, Recharts, framer-motion, Zustand, Zod, Bun) is validated and unchanged.
|
||||
---
|
||||
|
||||
## Recommended Stack
|
||||
## Context: What Already Exists (Do Not Re-Research)
|
||||
|
||||
### Authentication -- Logto (Self-Hosted)
|
||||
The following are validated and in production at v2.0. This file covers ADDITIONS AND CHANGES only.
|
||||
|
||||
| Technology | Version | Purpose | Why Recommended |
|
||||
|------------|---------|---------|-----------------|
|
||||
| Logto OSS | v1.36+ | External OIDC/OAuth 2.1 auth provider | TypeScript-native, purpose-built for app auth (not enterprise IAM), requires Postgres (shared infra), beautiful pre-built sign-in UI, React SDK with hooks, lightweight JWT validation on backend. MIT-licensed core. |
|
||||
| @logto/react | ^4.0.13 | React SDK for auth flows | LogtoProvider wraps app, provides useLogto() hook for sign-in/sign-out/token access. Handles OIDC redirect flow, token refresh, and user info. |
|
||||
| jose | ^6.2.2 | JWT validation on Hono backend | Zero-dependency, Bun-compatible, used to verify Logto-issued access tokens via JWKS. Recommended by Logto docs over heavier alternatives. |
|
||||
| Layer | Current |
|
||||
|-------|---------|
|
||||
| Runtime | Bun |
|
||||
| Frontend | React 19, TanStack Router/Query v5, Tailwind CSS v4, Zustand, Zod 4.x, framer-motion, Recharts, Lucide React |
|
||||
| Backend | Hono 4.12.x, Drizzle ORM 0.45.x, PostgreSQL (postgres.js 3.4.x driver) |
|
||||
| Auth | @hono/oidc-auth 1.8.x (Logto), API key auth, MCP OAuth 2.1 |
|
||||
| Storage | @aws-sdk/client-s3 3.x (MinIO) |
|
||||
| MCP | @modelcontextprotocol/sdk 1.29.x (19 tools) |
|
||||
| Rate limiting | Custom in-process Map (auth endpoints only, 5 req/15 min per IP) |
|
||||
|
||||
**Why Logto over alternatives:**
|
||||
---
|
||||
|
||||
| Provider | Why Not |
|
||||
|----------|---------|
|
||||
| Authentik | Python-based, heavyweight (designed for enterprise proxy/SSO), overkill for app-level auth. No React SDK -- requires raw OIDC integration. Better for infra-level SSO (Portainer, Grafana). |
|
||||
| Zitadel | Go-based, Kubernetes-first architecture, AGPL 3.0 license (copyleft since 2025). Stronger for multi-tenant B2B SaaS. Over-engineered for a single-product platform. |
|
||||
| SuperTokens | Session-based by default (not OIDC), requires embedding their middleware into your backend. Tighter coupling than external provider model. |
|
||||
| Keycloak | Java-based, heavy memory footprint (1-2GB RAM), complex admin UI. Industry standard but vastly over-scoped for this use case. |
|
||||
## New Capability Areas
|
||||
|
||||
**Integration pattern:** Logto runs as a separate Docker container alongside Postgres. React app redirects to Logto's hosted sign-in page for auth flows. Hono backend validates JWT access tokens from the Authorization header using `jose` JWKS verification -- no Logto SDK needed on the backend, just standard OIDC token validation. User identity is the Logto `sub` claim (a stable string ID), stored as `userId` on all user-owned records.
|
||||
### 1. Public Access Auth Model
|
||||
|
||||
**Backend middleware pattern (Hono):**
|
||||
**What's needed:** The `requireAuth` middleware in `src/server/middleware/auth.ts` already handles three auth paths (API key, OAuth Bearer, OIDC session). The skip-list pattern in `src/server/index.ts` already exempts public GETs on `/api/global-items`, `/api/tags`, `/api/users/:id/profile`, and `/api/setups/:id/public`.
|
||||
|
||||
**This milestone extends the skip-list** to cover new discovery endpoints (`/api/discovery/*`). Additionally, a new `tryAuth` middleware variant is needed for endpoints that work for both anonymous and authenticated users — it resolves `userId` if credentials are present but does NOT 401 on absence. This enables auth-aware responses (e.g., annotating feed items with "in your collection" for logged-in users).
|
||||
|
||||
**No new dependency.** Pure middleware logic — add `tryAuth` to `auth.ts`, update skip-list in `index.ts`.
|
||||
|
||||
---
|
||||
|
||||
### 2. Discovery Feed (Popular Setups, Trending Items)
|
||||
|
||||
The feed requires: ranked/scored queries, cursor-based pagination, and cheap repeated reads by anonymous users.
|
||||
|
||||
#### Trending Score
|
||||
|
||||
Use a hot-score computed in PostgreSQL SQL — no external search engine or materialized view needed at this scale.
|
||||
|
||||
```sql
|
||||
-- Hacker News-style decay: engagement / time^gravity
|
||||
SELECT id, brand, model,
|
||||
(owner_count::float / power((extract(epoch from now()) - extract(epoch from created_at)) / 3600.0 + 2, 1.8)) AS hot_score
|
||||
FROM global_items
|
||||
ORDER BY hot_score DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
This requires `ownerCount` as a real column (not a JOIN-time COUNT) on `globalItems`. The column already logically exists via join — promote it to a denormalized integer that the collection add/remove service path updates. No trigger needed; update it in the same database transaction as the collection operation.
|
||||
|
||||
**No new dependency.** Schema migration + service-layer update.
|
||||
|
||||
#### Cursor-Based Pagination
|
||||
|
||||
Drizzle ORM 0.45.x has documented cursor pagination support (two-column keyset). Use `(hotScore DESC, id DESC)` for the trending feed and `(createdAt DESC, id DESC)` for "recently added." Encode cursor as base64 JSON — opaque to the client.
|
||||
|
||||
The Hono + Drizzle cursor pattern is documented and actively used in the ecosystem. No pagination library needed.
|
||||
|
||||
**No new dependency.** Drizzle already supports this natively.
|
||||
|
||||
#### Full-Text Catalog Search
|
||||
|
||||
`globalItems` needs fast free-text search across `brand + model + description`. Use PostgreSQL native `tsvector` with a GIN index.
|
||||
|
||||
Drizzle 0.45.x does not generate `GENERATED ALWAYS AS ... STORED` syntax for tsvector columns in drizzle-kit. Add the `searchVector` column and GIN index via a raw SQL migration file (create via `drizzle-kit generate` then manually add the ALTER TABLE and CREATE INDEX statements to the generated file).
|
||||
|
||||
For the Hono route, use Drizzle's `sql` template tag with `to_tsquery`:
|
||||
|
||||
```typescript
|
||||
import { createRemoteJWKSet, jwtVerify } from "jose";
|
||||
.where(sql`search_vector @@ plainto_tsquery('english', ${q})`)
|
||||
.orderBy(sql`ts_rank(search_vector, plainto_tsquery('english', ${q})) DESC`)
|
||||
```
|
||||
|
||||
const jwks = createRemoteJWKSet(
|
||||
new URL("https://logto.example.com/oidc/jwks")
|
||||
);
|
||||
**No new dependency.** Schema migration + raw SQL in service layer.
|
||||
|
||||
const authMiddleware = createMiddleware(async (c, next) => {
|
||||
const token = c.req.header("Authorization")?.replace("Bearer ", "");
|
||||
if (!token) return c.json({ error: "Unauthorized" }, 401);
|
||||
#### Feed Client (TanStack Query + IntersectionObserver)
|
||||
|
||||
const { payload } = await jwtVerify(token, jwks, {
|
||||
issuer: "https://logto.example.com/oidc",
|
||||
audience: "your-api-resource-indicator",
|
||||
});
|
||||
`useInfiniteQuery` from `@tanstack/react-query` (already at 5.90.x) handles cursor pagination natively via `getNextPageParam`. The scroll trigger uses the browser-native IntersectionObserver API — implement a `useIntersectionObserver(ref, callback)` hook (~12 lines) rather than adding a scroll library. This matches the existing GearBox pattern of minimal third-party UI dependencies.
|
||||
|
||||
c.set("userId", payload.sub);
|
||||
await next();
|
||||
**No new dependency.**
|
||||
|
||||
---
|
||||
|
||||
### 3. Catalog Enrichment Infrastructure
|
||||
|
||||
#### Schema Additions to `globalItems`
|
||||
|
||||
New fields for attribution, source tracking, and feed ranking:
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|-------|------|---------|
|
||||
| `sourceUrl` | `text` | Canonical product page (retailer or manufacturer) |
|
||||
| `sourceAttribution` | `text` | Human-readable credit ("via REI", "via manufacturer") |
|
||||
| `imageAttributionUrl` | `text` | URL where product image was originally sourced |
|
||||
| `imageAttributionText` | `text` | License or credit line for the image |
|
||||
| `submittedByUserId` | `integer FK → users` | Who submitted this catalog entry (null = seeded by admin/agent) |
|
||||
| `verifiedAt` | `timestamp` | When an admin approved the entry (null = unverified) |
|
||||
| `ownerCount` | `integer NOT NULL DEFAULT 0` | Denormalized count of collection items referencing this |
|
||||
| `productUrl` | `text` | Retailer/manufacturer product link (duplicates item-level, but catalog-owned) |
|
||||
|
||||
These are Drizzle schema additions. **No new dependency.**
|
||||
|
||||
#### Zod Schemas for Enriched Catalog
|
||||
|
||||
Add `CreateCatalogItemSchema` in `src/shared/schemas.ts` with attribution fields. Zod 4.3.x handles this natively. The schema feeds the new `POST /api/global-items` route (currently only GET is public — writes will require auth but open to non-admins for catalog submissions).
|
||||
|
||||
---
|
||||
|
||||
### 4. Agent-Powered Catalog Seeding via MCP
|
||||
|
||||
The existing MCP server (`@modelcontextprotocol/sdk` 1.29.x, 19 tools) already provides the infrastructure. The agent workflow:
|
||||
|
||||
1. Claude agent receives a category or brand as a prompt
|
||||
2. Uses a new `create_catalog_item` MCP tool — purpose-built for `globalItems` insertion with full attribution fields
|
||||
3. Server validates via Zod, inserts into `globalItems`, updates `ownerCount` denormalization
|
||||
4. Agent uses the existing `upload_image_from_url` tool to fetch and store product images
|
||||
|
||||
The new tool registers identically to existing tools in `src/server/mcp/index.ts`. Batch seeding sessions: the agent runs N `create_catalog_item` calls in sequence within one MCP session — no parallel execution framework needed at catalog bootstrap scale.
|
||||
|
||||
For standalone seed scripts (`bun run src/db/dev-seed.ts` extensions), use the Drizzle db instance directly. No external seeding framework.
|
||||
|
||||
**No new dependency.**
|
||||
|
||||
---
|
||||
|
||||
### 5. HTTP Caching for Public Endpoints
|
||||
|
||||
Public GET endpoints (discovery feed, catalog detail pages) will be hit by anonymous users repeatedly. Add HTTP-level cache hints to reduce DB round-trips.
|
||||
|
||||
- **Catalog item detail pages** (`GET /api/global-items/:id`): Use Hono's built-in `etag()` middleware. Content-addressed — returns 304 Not Modified when item hasn't changed.
|
||||
- **Discovery feed endpoints** (`GET /api/discovery/*`): Set `Cache-Control: public, max-age=60, stale-while-revalidate=300` manually in route handlers. Feed data tolerates 60s staleness.
|
||||
|
||||
**Do NOT use Hono's `cache()` middleware** — it is platform-specific to Cloudflare Workers and Deno, and silently does nothing on Bun. This is a documented limitation. Known issue #4401 in the Hono repo also shows the `etag()` middleware can generate inconsistent ETags when combining with other middleware — test in integration tests before shipping.
|
||||
|
||||
**No new dependency.** `etag` is built into Hono 4.12.x.
|
||||
|
||||
---
|
||||
|
||||
### 6. Rate Limiting for Public Traffic
|
||||
|
||||
The existing `rateLimit.ts` in-process Map handles auth endpoints correctly (5 req/15 min per IP). It is inappropriate for public discovery traffic because:
|
||||
|
||||
- 5 req/15 min is far too strict for anonymous browsing
|
||||
- In-process state resets on server restart (tolerable for auth, wrong for general rate limiting)
|
||||
- No way to differentiate authenticated vs anonymous callers in the current implementation
|
||||
|
||||
**Recommendation:** Keep the existing `rateLimit.ts` for auth endpoints only. Add `hono-rate-limiter` for discovery/catalog public endpoints with a permissive anonymous limit (e.g., 100 req/min per IP) and no limit for authenticated callers.
|
||||
|
||||
```typescript
|
||||
import { rateLimiter } from "hono-rate-limiter";
|
||||
|
||||
const discoveryLimiter = rateLimiter({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
limit: 100,
|
||||
keyGenerator: (c) => c.req.header("x-forwarded-for")?.split(",")[0] ?? "unknown",
|
||||
});
|
||||
|
||||
app.use("/api/discovery/*", discoveryLimiter);
|
||||
```
|
||||
|
||||
**React provider pattern:**
|
||||
The in-process storage adapter (default in `hono-rate-limiter`) is sufficient for single-instance deployment. If the app scales horizontally, swap to `@hono-rate-limiter/redis` — but that is a future decision, not a v2.1 concern.
|
||||
|
||||
```typescript
|
||||
import { LogtoProvider, LogtoConfig } from "@logto/react";
|
||||
**New dependency:**
|
||||
|
||||
const config: LogtoConfig = {
|
||||
endpoint: "https://logto.example.com",
|
||||
appId: "<your-app-id>",
|
||||
resources: ["https://api.gearbox.example.com"],
|
||||
};
|
||||
|
||||
// Wrap app root
|
||||
<LogtoProvider config={config}>
|
||||
<App />
|
||||
</LogtoProvider>
|
||||
```
|
||||
|
||||
### Database -- PostgreSQL via Bun Native Driver
|
||||
|
||||
| Technology | Version | Purpose | Why Recommended |
|
||||
|------------|---------|---------|-----------------|
|
||||
| PostgreSQL | 16+ | Primary database | Required by Logto anyway, proper concurrent access for multi-user, JSONB for flexible spec fields, full-text search for discovery feed. |
|
||||
| drizzle-orm | ^0.45.1 (existing) | Type-safe ORM | Already in use. Switch from `drizzle-orm/bun-sqlite` to `drizzle-orm/bun-sql` for Postgres. Schema definitions move from `sqlite-core` to `pg-core`. |
|
||||
| Bun native SQL | built-in | Postgres driver | Zero additional dependencies. `import { SQL } from "bun"` provides native Postgres bindings. Drizzle ORM supports it via `drizzle-orm/bun-sql`. |
|
||||
| postgres (postgres.js) | ^3.4.8 | Fallback Postgres driver | Only needed if Bun native SQL has issues with drizzle-kit CLI tooling (known issue #4122). More mature ecosystem, proven with Drizzle. Install as dev dependency for drizzle-kit. |
|
||||
|
||||
**Schema migration approach:**
|
||||
|
||||
1. Rewrite `src/db/schema.ts` imports from `drizzle-orm/sqlite-core` to `drizzle-orm/pg-core`
|
||||
2. Replace `sqliteTable` with `pgTable`
|
||||
3. Replace `integer().primaryKey({ autoIncrement: true })` with `integer().primaryKey().generatedAlwaysAsIdentity()` for PKs
|
||||
4. Replace `integer("created_at", { mode: "timestamp" })` with `timestamp("created_at").defaultNow().notNull()`
|
||||
5. Add `userId text("user_id").notNull()` to all user-owned tables (items, threads, setups, categories)
|
||||
6. Add `visibility text("visibility").notNull().default("private")` to setups and profiles
|
||||
7. Generate fresh Postgres migration with `drizzle-kit generate`
|
||||
8. Write a one-time data migration script (SQLite read -> Postgres insert) for existing data
|
||||
|
||||
**drizzle.config.ts change:**
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
{ dialect: "sqlite", dbCredentials: { url: "./gearbox.db" } }
|
||||
|
||||
// After
|
||||
{ dialect: "postgresql", dbCredentials: { url: process.env.DATABASE_URL } }
|
||||
```
|
||||
|
||||
**Known issue:** drizzle-kit CLI does not use the Bun SQL driver for `push`/`generate` commands (GitHub issue #4122). Workaround: install `postgres` (postgres.js) as a dev dependency for drizzle-kit, while the app runtime uses Bun native SQL.
|
||||
|
||||
### Image Storage -- Bun Native S3 + MinIO
|
||||
|
||||
| Technology | Version | Purpose | Why Recommended |
|
||||
|------------|---------|---------|-----------------|
|
||||
| Bun S3Client | built-in | S3 API client | Zero dependencies, native Bun bindings, extends Blob interface. Supports presigned URLs, streaming uploads. Built-in MinIO compatibility. |
|
||||
| MinIO | latest | Self-hosted S3-compatible object storage | Replaces local `./uploads/` directory. Single Go binary, Docker-friendly, S3 API compatible. Handles multi-user image scaling without cloud vendor lock-in. |
|
||||
|
||||
**Why Bun native S3 over @aws-sdk/client-s3:**
|
||||
|
||||
- Zero additional dependencies (Bun ships with it)
|
||||
- Simpler API (extends Blob, web-standard patterns)
|
||||
- Native performance bindings
|
||||
- Full MinIO compatibility documented by Bun team
|
||||
|
||||
**Migration from ./uploads/:**
|
||||
|
||||
1. Deploy MinIO container alongside app
|
||||
2. Create `gearbox-images` bucket
|
||||
3. Write migration script to upload existing files from `./uploads/` to MinIO
|
||||
4. Update image service to use S3Client for reads/writes
|
||||
5. Serve images via presigned URLs or a proxy route on Hono
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```typescript
|
||||
import { S3Client } from "bun";
|
||||
|
||||
const storage = new S3Client({
|
||||
accessKeyId: process.env.S3_ACCESS_KEY!,
|
||||
secretAccessKey: process.env.S3_SECRET_KEY!,
|
||||
bucket: "gearbox-images",
|
||||
endpoint: process.env.S3_ENDPOINT!, // e.g., http://minio:9000
|
||||
});
|
||||
```
|
||||
|
||||
### Supporting Libraries
|
||||
|
||||
| Library | Version | Purpose | When to Use |
|
||||
|---------|---------|---------|-------------|
|
||||
| jose | ^6.2.2 | JWKS-based JWT verification | Every authenticated API request -- validate Logto access tokens on Hono middleware |
|
||||
| @logto/react | ^4.0.13 | React auth provider + hooks | Wrap app root, sign-in/sign-out flows, access token retrieval for API calls |
|
||||
|
||||
### Development / Infrastructure
|
||||
|
||||
| Tool | Purpose | Notes |
|
||||
|------|---------|-------|
|
||||
| Docker Compose | Local dev environment | Postgres + Logto + MinIO containers. App still runs on bare Bun for HMR. |
|
||||
| drizzle-kit | Schema management | Same tool, different dialect config. `bun run db:generate` and `bun run db:push` still work. |
|
||||
|
||||
## Installation
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| `hono-rate-limiter` | `^0.5.3` | Per-route rate limiting with configurable windows for public endpoints |
|
||||
|
||||
```bash
|
||||
# New production dependencies
|
||||
bun add @logto/react jose
|
||||
|
||||
# New dev dependencies (for drizzle-kit Postgres support)
|
||||
bun add -D postgres
|
||||
|
||||
# No install needed for:
|
||||
# - Bun native S3 (built-in)
|
||||
# - Bun native SQL/Postgres (built-in)
|
||||
# - drizzle-orm (already installed, just change imports)
|
||||
bun add hono-rate-limiter
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Full Stack Additions Summary
|
||||
|
||||
### New Dependencies (v2.1 only)
|
||||
|
||||
| Library | Version | Purpose | Why |
|
||||
|---------|---------|---------|-----|
|
||||
| `hono-rate-limiter` | `^0.5.3` | Configurable rate limits for public discovery routes | Existing in-process limiter is auth-only with a 5-req cap; public browse traffic needs separate, permissive limits |
|
||||
|
||||
### No New Dependencies Needed For
|
||||
|
||||
| Capability | Existing Stack Component Used |
|
||||
|------------|------------------------------|
|
||||
| Public auth model (`tryAuth` variant) | Hono middleware — no library |
|
||||
| Discovery feed cursor pagination | Drizzle 0.45.x cursor pagination docs |
|
||||
| Full-text catalog search (tsvector GIN) | PostgreSQL native + Drizzle `sql` template |
|
||||
| Trending score computation | PostgreSQL SQL expression — no extension |
|
||||
| Infinite scroll client | TanStack Query `useInfiniteQuery` + native IntersectionObserver |
|
||||
| Catalog attribution fields | Drizzle schema migration |
|
||||
| Agent catalog seeding | Existing MCP SDK + new `create_catalog_item` tool |
|
||||
| HTTP cache headers | Hono built-in `etag()` + manual `Cache-Control` |
|
||||
| Feed ranking denormalization | Service-layer transaction update (no trigger, no cron) |
|
||||
|
||||
---
|
||||
|
||||
## Schema Changes Required (Not Library Changes)
|
||||
|
||||
These are Drizzle schema additions generating migrations:
|
||||
|
||||
### `globalItems` additions
|
||||
|
||||
```typescript
|
||||
// In src/db/schema.ts — globalItems table additions
|
||||
sourceUrl: text("source_url"),
|
||||
sourceAttribution: text("source_attribution"),
|
||||
imageAttributionUrl: text("image_attribution_url"),
|
||||
imageAttributionText: text("image_attribution_text"),
|
||||
submittedByUserId: integer("submitted_by_user_id").references(() => users.id),
|
||||
verifiedAt: timestamp("verified_at"),
|
||||
ownerCount: integer("owner_count").notNull().default(0),
|
||||
productUrl: text("product_url"),
|
||||
```
|
||||
|
||||
### Raw SQL migration additions (cannot be expressed in Drizzle schema)
|
||||
|
||||
```sql
|
||||
-- Add after Drizzle-generated migration runs:
|
||||
|
||||
-- Generated tsvector column for full-text search
|
||||
ALTER TABLE global_items
|
||||
ADD COLUMN search_vector tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
to_tsvector('english',
|
||||
coalesce(brand, '') || ' ' ||
|
||||
coalesce(model, '') || ' ' ||
|
||||
coalesce(description, '')
|
||||
)
|
||||
) STORED;
|
||||
|
||||
CREATE INDEX global_items_search_vector_idx ON global_items USING GIN(search_vector);
|
||||
|
||||
-- Partial index for public setup discovery feed
|
||||
CREATE INDEX setups_public_updated_idx ON setups (updated_at DESC) WHERE is_public = true;
|
||||
|
||||
-- Trending feed index
|
||||
CREATE INDEX global_items_owner_count_id_idx ON global_items (owner_count DESC, id DESC);
|
||||
```
|
||||
|
||||
> **Note:** Drizzle Kit does not generate `GENERATED ALWAYS AS ... STORED` for tsvector. Add these as a separate raw SQL file appended to the Drizzle migration or as a separate `customMigration` file in the migrations folder. Run via `bun run db:push` after the Drizzle migration applies.
|
||||
|
||||
### `setups` additions
|
||||
|
||||
```typescript
|
||||
// In src/db/schema.ts — setups table additions
|
||||
viewCount: integer("view_count").notNull().default(0),
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Authentication Provider
|
||||
| Recommended | Alternative | Why Not |
|
||||
|-------------|-------------|---------|
|
||||
| PostgreSQL tsvector + GIN | Meilisearch / Typesense | Separate search service adds infra ops complexity; tsvector covers structured gear catalog search at GearBox scale without additional containers |
|
||||
| PostgreSQL tsvector + GIN | pg_textsearch (BM25 extension) | Requires installing a PostgreSQL extension in production; BM25 ranking is unnecessary for a catalog of branded products where exact brand/model matches dominate |
|
||||
| Denormalized `ownerCount` column | COUNT JOIN per feed request | Feed queries fire on every anonymous page load; a JOIN COUNT becomes a bottleneck before any other part of the stack does |
|
||||
| Native IntersectionObserver hook | react-infinite-scroll-component | Zero-dependency — 12-line hook replaces an 8KB library; consistent with GearBox's minimal-external-dependency UI philosophy |
|
||||
| Manual `Cache-Control` headers | Hono `cache()` middleware | Hono `cache()` is Cloudflare Workers/Deno only — silently does nothing on Bun |
|
||||
| `hono-rate-limiter` in-process | Redis-backed rate limiter | Single-instance deployment — Redis adds an infra dependency not justified at current scale |
|
||||
| Extend existing MCP toolset | Separate seeding CLI script | MCP agents already have auth and structured tool calling; a dedicated `create_catalog_item` tool is cleaner than a one-off script that bypasses the service layer |
|
||||
| Service-layer `ownerCount` update | PostgreSQL trigger | Triggers are invisible to the TypeScript codebase, harder to test, and prone to silent failures in complex transactions |
|
||||
|
||||
| Recommended | Alternative | When to Use Alternative |
|
||||
|-------------|-------------|-------------------------|
|
||||
| Logto | Authentik | If you need proxy-mode SSO for non-OIDC apps (Portainer, legacy tools) |
|
||||
| Logto | Zitadel | If building multi-tenant B2B SaaS with organization-level isolation |
|
||||
| Logto | Keycloak | If enterprise LDAP/AD integration is mandatory |
|
||||
|
||||
### Database Driver
|
||||
|
||||
| Recommended | Alternative | When to Use Alternative |
|
||||
|-------------|-------------|-------------------------|
|
||||
| Bun native SQL (`bun:sql`) | postgres.js | If Bun native SQL has concurrency bugs (known issue in Bun 1.2.0 with concurrent statements) |
|
||||
| Bun native SQL (`bun:sql`) | @neondatabase/serverless | If deploying to serverless/edge where persistent connections are not possible |
|
||||
|
||||
### Image Storage
|
||||
|
||||
| Recommended | Alternative | When to Use Alternative |
|
||||
|-------------|-------------|-------------------------|
|
||||
| MinIO (self-hosted) | Cloudflare R2 | If you want zero-ops storage with no egress fees and don't mind cloud dependency |
|
||||
| MinIO (self-hosted) | Local filesystem (current) | For development/testing only. Not viable for multi-user at scale. |
|
||||
---
|
||||
|
||||
## What NOT to Add
|
||||
|
||||
| Avoid | Why | Use Instead |
|
||||
|-------|-----|-------------|
|
||||
| @aws-sdk/client-s3 | 60+ transitive dependencies, Bun has native S3 support | Bun built-in S3Client |
|
||||
| passport.js / express-session | Wrong paradigm -- we want external OIDC, not embedded session auth | Logto + jose JWT validation |
|
||||
| next-auth / auth.js | Designed for Next.js, assumes framework integration we don't have | Logto (external provider) |
|
||||
| better-auth | Embedded auth library, opposite of external provider model | Logto (external provider) |
|
||||
| pg (node-postgres) | Callback-based API, Bun has native Postgres bindings | Bun native SQL or postgres.js |
|
||||
| sharp / image processing libs | Premature optimization -- serve originals first, add resizing later if needed | Direct S3 storage of originals |
|
||||
| Redis | Not needed at this scale. Postgres handles sessions (via Logto), caching is premature | Postgres for everything |
|
||||
| Prisma | Already using Drizzle ORM, no reason to add a second ORM | drizzle-orm (existing) |
|
||||
| nanoid / cuid2 | Postgres `gen_random_uuid()` is built-in for public-facing IDs if needed | Postgres native UUID generation |
|
||||
| TypeORM / Sequelize | Legacy ORMs with worse TypeScript support than Drizzle | drizzle-orm (existing) |
|
||||
| Elasticsearch / OpenSearch | Separate cluster, ops overhead, overkill for a structured product catalog | PostgreSQL tsvector with GIN index |
|
||||
| pg_textsearch / VectorChord-BM25 | PostgreSQL extension install required in prod; BM25 precision unnecessary for brand+model search | PostgreSQL native `ts_rank` |
|
||||
| Hono `cache()` middleware | Platform-specific to Cloudflare/Deno; does nothing on Bun | Manual `Cache-Control` headers in route handlers |
|
||||
| react-virtual / windowing | Feed is paginated, not a virtual list; items per page (~20) never hit DOM performance limits | Standard DOM list with cursor pagination |
|
||||
| Prisma | Already using Drizzle ORM; two ORMs in one codebase is a maintenance trap | drizzle-orm (existing) |
|
||||
| Materialized views for feed caching | drizzle-kit does not fully support materialized view migrations; manual REFRESH logic is brittle | Denormalized score columns + partial indexes |
|
||||
| Separate seeding framework (Faker, etc.) | Catalog data is real product data, not fake; agent seeding produces real structured records | MCP `create_catalog_item` tool |
|
||||
|
||||
## Infrastructure Architecture
|
||||
|
||||
```
|
||||
Docker Compose (dev) / Docker (prod)
|
||||
+-- gearbox-app (Bun, port 3000)
|
||||
+-- gearbox-postgres (PostgreSQL 16, port 5432)
|
||||
| +-- gearbox DB (app data)
|
||||
| +-- logto DB (Logto data, separate database same instance)
|
||||
+-- gearbox-logto (Logto OSS, port 3001 app / 3002 admin)
|
||||
+-- gearbox-minio (MinIO, port 9000 API / 9001 console)
|
||||
```
|
||||
|
||||
Logto and the app share a single Postgres instance (different databases). This keeps infrastructure simple -- one Postgres to back up, one to monitor. Logto requires PostgreSQL 14+; using 16 covers both.
|
||||
---
|
||||
|
||||
## Version Compatibility
|
||||
|
||||
| Package | Compatible With | Notes |
|
||||
|---------|-----------------|-------|
|
||||
| drizzle-orm@0.45.x | Bun native SQL | Supported via `drizzle-orm/bun-sql` driver |
|
||||
| drizzle-orm@0.45.x | postgres.js@3.4.x | Supported via `drizzle-orm/postgres-js` driver (fallback) |
|
||||
| drizzle-kit@0.31.x | PostgreSQL 16 | Generates Postgres-dialect migrations |
|
||||
| @logto/react@4.x | React 19 | Uses React context/hooks, compatible |
|
||||
| jose@6.x | Bun runtime | Explicitly lists Bun support in docs |
|
||||
| Logto OSS v1.36 | PostgreSQL 14+ | Logto requires PG 14 minimum; use PG 16 for both app and Logto |
|
||||
| Bun S3Client | MinIO latest | Documented compatibility with endpoint configuration |
|
||||
| Package | Current Version | v2.1 Notes |
|
||||
|---------|----------------|------------|
|
||||
| `hono` | 4.12.x (4.12.12 latest) | `etag()` built-in available; `cache()` is NOT compatible with Bun — do not use |
|
||||
| `drizzle-orm` | 0.45.x (0.45.2 latest stable) | Cursor pagination confirmed; generated tsvector column requires raw SQL migration appended to drizzle-kit output |
|
||||
| `@tanstack/react-query` | 5.90.x | `useInfiniteQuery` with `getNextPageParam` fully supports cursor pattern natively |
|
||||
| `hono-rate-limiter` | 0.5.3 (latest, published ~16 days ago) | In-process storage adapter works on Bun; actively maintained |
|
||||
| `@modelcontextprotocol/sdk` | 1.29.x | Existing MCP tooling is sufficient for adding `create_catalog_item` |
|
||||
| `zod` | 4.3.x | New catalog attribution schemas are straightforward additions to existing `schemas.ts` |
|
||||
| `@hono/zod-validator` | 0.7.x | Already used for all routes; covers new discovery/catalog endpoints |
|
||||
|
||||
## Migration Checklist (SQLite to Postgres)
|
||||
---
|
||||
|
||||
1. **Schema rewrite**: `sqlite-core` -> `pg-core` imports, adjust column types
|
||||
2. **Driver swap**: `drizzle-orm/bun-sqlite` -> `drizzle-orm/bun-sql`
|
||||
3. **Config update**: `drizzle.config.ts` dialect and credentials
|
||||
4. **Fresh migrations**: Generate from scratch for Postgres (do not try to convert SQLite migrations)
|
||||
5. **Data migration**: One-time script reads SQLite, writes to Postgres
|
||||
6. **Test infrastructure**: Update `createTestDb()` helper to use Postgres test database (or pg-mem for in-memory testing)
|
||||
7. **CI pipeline**: Add Postgres service container for test runs
|
||||
8. **Remove SQLite deps**: Remove `better-sqlite3` from devDependencies after migration confirmed
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Only one new package for v2.1
|
||||
bun add hono-rate-limiter
|
||||
```
|
||||
|
||||
Everything else is schema migrations, new service/route/middleware code, and one new MCP tool — all on the existing stack.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- [Logto official docs -- React quickstart](https://docs.logto.io/quick-starts/react) -- SDK setup, LogtoProvider config (HIGH confidence)
|
||||
- [Logto API protection -- JWT validation](https://docs.logto.io/api-protection/nodejs/express) -- jose-based middleware pattern (HIGH confidence)
|
||||
- [Logto OSS getting started](https://docs.logto.io/logto-oss/get-started-with-oss) -- Docker deployment, Postgres requirements (HIGH confidence)
|
||||
- [Logto @logto/react npm](https://www.npmjs.com/package/@logto/react) -- Version 4.0.13 confirmed (HIGH confidence)
|
||||
- [Drizzle ORM -- Bun SQL driver](https://orm.drizzle.team/docs/connect-bun-sql) -- Native Postgres via Bun (HIGH confidence)
|
||||
- [Drizzle ORM -- PostgreSQL column types](https://orm.drizzle.team/docs/column-types/pg) -- pg-core schema definitions (HIGH confidence)
|
||||
- [drizzle-kit Bun SQL issue #4122](https://github.com/drizzle-team/drizzle-orm/issues/4122) -- Known CLI limitation with Bun driver (MEDIUM confidence)
|
||||
- [Bun S3 documentation](https://bun.com/docs/runtime/s3) -- Native S3 client, MinIO config (HIGH confidence)
|
||||
- [MinIO GitHub](https://github.com/minio/minio) -- S3-compatible self-hosted storage (HIGH confidence)
|
||||
- [jose GitHub](https://github.com/panva/jose) -- JWT library v6.2.2, explicit Bun support (HIGH confidence)
|
||||
- [Authentik vs Zitadel comparison](https://wz-it.com/en/blog/authentik-vs-zitadel-identity-provider-comparison/) -- Auth provider analysis (MEDIUM confidence)
|
||||
- [Keycloak vs Authentik vs Zitadel 2026](https://blog.houseoffoss.com/post/keycloak-vs-authentik-vs-zitadel-2026-which-open-source-login-tool-should-you-use) -- Ecosystem overview (MEDIUM confidence)
|
||||
- [postgres.js npm](https://www.npmjs.com/package/postgres) -- Version 3.4.8, fallback driver (HIGH confidence)
|
||||
- [Drizzle ORM cursor-based pagination](https://orm.drizzle.team/docs/guides/cursor-based-pagination) — two-column keyset pattern, v0.45.x confirmed (HIGH)
|
||||
- [Drizzle ORM PostgreSQL full-text search](https://orm.drizzle.team/docs/guides/postgresql-full-text-search) — tsvector approach confirmed (HIGH)
|
||||
- [Drizzle ORM full-text search with generated columns](https://orm.drizzle.team/docs/guides/full-text-search-with-generated-columns) — generated column pattern for tsvector (HIGH)
|
||||
- [Hono ETag middleware](https://hono.dev/docs/middleware/builtin/etag) — built-in, no install required (HIGH)
|
||||
- [Hono Cache middleware](https://hono.dev/docs/middleware/builtin/cache) — explicitly listed as Cloudflare/Deno only, not Bun (HIGH)
|
||||
- [Hono ETag issue #4401](https://github.com/honojs/hono/issues/4401) — known inconsistency bug in etag middleware (MEDIUM)
|
||||
- [hono-rate-limiter GitHub](https://github.com/rhinobase/hono-rate-limiter) — v0.5.3, active, Bun compatible (HIGH)
|
||||
- [hono-rate-limiter npm](https://www.npmjs.com/package/hono-rate-limiter) — version 0.5.3 confirmed (HIGH)
|
||||
- [TanStack Query infinite queries](https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries) — `useInfiniteQuery` cursor pattern (HIGH)
|
||||
- [Drizzle ORM materialized views issue #2653](https://github.com/drizzle-team/drizzle-orm/issues/2653) — confirmed drizzle-kit does not fully support materialized view migrations (MEDIUM)
|
||||
- [Hono middleware docs](https://hono.dev/docs/guides/middleware) — selective auth middleware pattern (HIGH)
|
||||
- GearBox `package.json` — all existing dependency versions verified directly (HIGH)
|
||||
- GearBox `src/server/index.ts` — existing skip-list pattern verified directly (HIGH)
|
||||
- GearBox `src/server/middleware/auth.ts` — existing three-way auth verified directly (HIGH)
|
||||
- GearBox `src/db/schema.ts` — existing `globalItems` table columns verified directly (HIGH)
|
||||
|
||||
---
|
||||
*Stack research for: GearBox v2.0 Platform Foundation*
|
||||
*Researched: 2026-04-03*
|
||||
|
||||
*Stack research for: GearBox v2.1 Public Discovery milestone*
|
||||
*Researched: 2026-04-09*
|
||||
|
||||
Reference in New Issue
Block a user