docs: complete project research

This commit is contained in:
2026-04-09 14:44:12 +02:00
parent f9c69a1366
commit c4ad5c1b2a
4 changed files with 910 additions and 1592 deletions

View File

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