# Loam — MTB Build & Tune A Next.js web app and native Android client for mountain bike riders to configure builds, get manufacturer-verified suspension and tire setup starting points, save post-ride reviews, and receive AI-powered tuning recommendations based on personal preference history. The backend is built with Next.js App Router, Prisma/PostgreSQL, and Ollama for local LLM inference. The Android client is built with Kotlin, Jetpack Compose, Ktor, and Koin against the same JSON API. --- ## Tech stack | Layer | Tech | |---|---| | Framework | Next.js 16 (App Router) | | Language | TypeScript | | Database | PostgreSQL | | ORM | Prisma | | Auth | bcryptjs + jose (JWT in HTTP-only cookies) | | Images | sharp (layer compositing) | | AI | Ollama (local LLM) | | Validation | Zod | | Android | Kotlin, Jetpack Compose, Ktor, Koin | --- ## Features - **Catalog** — Curated frames, forks, shocks, and tires with real-world setup ranges - **Setup verification** — Every product carries a research audit trail (manufacturer docs, community sources, confidence levels) - **Build composer** — Layered bike image generation from selected components - **Per-user learning** — Builds, reviews, and learned preferences are scoped to authenticated users - **AI recommendations** — Ollama-powered tuning advice that incorporates the rider's own review history - **Post-ride reviews** — Rate suspension feel, grip, rolling speed, and report issues (bottom-outs, harsh hits, wallow, small-bump harshness) --- ## Getting started ### Prerequisites - Node.js 20+ - PostgreSQL running locally (or a remote instance) - Ollama installed and running (optional, for AI recommendations) ### 1. Install dependencies ```bash npm install ``` ### 2. Configure environment variables ```bash cp .env.example .env.local ``` Edit `.env.local`: ``` DATABASE_URL=postgresql://user:password@localhost:5432/loam JWT_SECRET=change-this-to-a-long-random-string-in-production # Optional — recommended local model shown OLLAMA_URL=http://localhost:11434 OLLAMA_MODEL=qwen3:32b ``` ### 3. Set up the database ```bash # Create and apply migrations npm run db:migrate # Seed product catalog npm run db:seed ``` ### 4. Pull an Ollama model (optional) ```bash ollama pull qwen3:32b ``` `qwen3:32b` is the recommended local model for the recommendation endpoint on a 32GB VRAM GPU such as the AMD Radeon AI PRO R9700. Use `qwen3:14b` if you want faster responses or need a smaller memory footprint. ### 5. Run the dev server ```bash npm run dev ``` Open [http://localhost:3000](http://localhost:3000). --- ## Docker Build and run the web app plus PostgreSQL: ```bash docker compose up --build ``` The app is exposed at [http://localhost:3333](http://localhost:3333). The Compose stack provides `DATABASE_URL` and `JWT_SECRET` for the app container. The Docker image installs dependencies with `npm ci --ignore-scripts`; Prisma generation runs later in the builder stage after `prisma/schema.prisma` has been copied into the image. The base image also installs OpenSSL so Prisma's native query engine works on Alpine. ### Ollama in a Separate Container The app expects Ollama at `OLLAMA_URL`. If Ollama runs in another container, expose port `11434` and point the app at that service or host address. For an AMD GPU with ROCm, a typical Ollama container launch is: ```bash docker run --rm -it \ --device=/dev/kfd \ --device=/dev/dri \ --group-add video \ --group-add render \ -v ollama:/root/.ollama \ -p 11434:11434 \ ollama/ollama:rocm ``` Then pull the recommended model: ```bash ollama pull qwen3:32b ``` If the app runs in Docker Compose and Ollama is bound to the host on Linux, set: ```yaml environment: OLLAMA_URL: http://host.docker.internal:11434 OLLAMA_MODEL: qwen3:32b extra_hosts: - "host.docker.internal:host-gateway" ``` --- ## Project structure ``` prisma/ schema.prisma # User, BikeBuild, RideReview, RiderPreference, Product seed.ts # Seeds catalog into Product table src/ app/ api/ auth/ # register, login, me, logout builds/ # list / create builds (scoped to user) reviews/ # create reviews (scoped to user + build ownership) ai/recommend # Ollama recommendation prompt images/compose # sharp layer compositing catalog/ # public catalog + setupVerification setup-audit/ # research audit endpoint components/ BikeBuilderApp.tsx lib/ auth.ts # bcrypt, JWT cookie session, requireAuth db.ts # per-user Prisma queries catalog.ts # static product data with citations recommendations.ts # setup math by rider profile setupVerification.ts # research audit metadata types.ts # shared TypeScript types public/ images/ # product assets + user-generated builds android/ app/src/main/java/com/bikeloam/app/ data/ # DTOs, Ktor API client, repository navigation/ # Compose navigation graph ui/ # Compose screens, theme, ViewModels app/src/test/ # JVM unit tests for repository + ViewModels ``` --- ## API routes | Route | Method | Auth | Description | |---|---|---|---| | `/api/auth/register` | POST | No | Create account, set session cookie | | `/api/auth/login` | POST | No | Log in, set session cookie | | `/api/auth/me` | GET | No | Return current session user | | `/api/auth/logout` | POST | No | Clear session cookie | | `/api/builds` | GET | Yes | List current user's builds | | `/api/builds` | POST | Yes | Save a new build | | `/api/reviews` | POST | Yes | Review a build (must own it) | | `/api/ai/recommend` | POST | Yes | AI tuning advice with user preferences | | `/api/images/compose` | POST | No | Compose build image from layers | | `/api/catalog` | GET | No | Full catalog + setupVerification | | `/api/setup-audit` | GET | No | Research audit report | --- ## Auth system - Passwords hashed with bcrypt (12 rounds) - Sessions are JWTs stored in HTTP-only `loam-session` cookies (7-day expiry) - `requireAuth()` guards data-modifying routes - All builds, reviews, and preferences are scoped to the authenticated `userId` --- ## Data model overview - **User** — email (unique), passwordHash, name - **BikeBuild** — belongs to User; stores component IDs, rider profile, setup JSON, image path - **RideReview** — belongs to User + BikeBuild; ratings and issue flags - **RiderPreference** — one per User; aggregated from review history (harsh/soft tendency, grip preference) - **Product** — seeded catalog of frames, forks, shocks, tires with specs and sources JSON --- ## Research & citations Every product entry includes a `sources` array and a `SetupVerification` record: - `manufacturer` — official setup guides, owner manuals, product pages - `community` — Pinkbike reviews, Reddit discussions, Blister/MTBR test notes - Status levels: `verified` → `manufacturer-guided` → `community-informed` → `estimated` The `/api/setup-audit` endpoint returns a full breakdown of confidence levels and missing sources. --- ## Android client The native Android app lives in [`android/`](android/README.md). It uses the same plain JSON API over HTTP with cookie-based auth: 1. Implementing cookie jar storage for the `loam-session` cookie 2. Calling `/api/auth/login` or `/api/auth/register` to obtain a session 3. Reusing `/api/builds`, `/api/reviews`, `/api/ai/recommend`, and `/api/catalog` as-is For local emulator development, the debug build points at `http://10.0.2.2:3000`. Start the web backend with `npm run dev`, then build or test the app from the Android project: ```bash cd android ./gradlew assembleDebug ./gradlew testDebugUnitTest ``` The image composition endpoint (`/api/images/compose`) returns a PNG path; mobile clients can either request server-side composition or replicate the layer-stacking logic locally. --- ## Scripts | Command | Purpose | |---|---| | `npm run dev` | Start Next.js dev server | | `npm run build` | Production build | | `npm run db:migrate` | Run Prisma migrations | | `npm run db:seed` | Seed product catalog | | `npm run db:studio` | Open Prisma Studio | | `npm run lint` | Run ESLint | Android commands are run from `android/`: | Command | Purpose | |---|---| | `./gradlew assembleDebug` | Build the debug APK | | `./gradlew installDebug` | Install debug build on a connected device or emulator | | `./gradlew testDebugUnitTest` | Run JVM unit tests | | `./gradlew connectedDebugAndroidTest` | Run instrumented Android tests | --- ## Environment variables | Variable | Required | Default | Description | |---|---|---|---| | `DATABASE_URL` | Yes | — | PostgreSQL connection string | | `JWT_SECRET` | Yes | — | Secret for signing session JWTs | | `OLLAMA_URL` | No | `http://localhost:11434` | Ollama server base URL | | `OLLAMA_MODEL` | No | `llama3.1:8b` | Model name for recommendations. `qwen3:32b` is recommended for a 32GB VRAM local GPU. |