Loam — MTB Build & Tune
A Next.js web app 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.
Built with Next.js App Router, Prisma/PostgreSQL, and Ollama for local LLM inference. The API layer is designed for reuse in a future React Native/Android client.
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 |
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
npm install
2. Configure environment variables
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 — defaults shown
OLLAMA_URL=http://localhost:11434
OLLAMA_MODEL=llama3.1:8b
3. Set up the database
# Create and apply migrations
npm run db:migrate
# Seed product catalog
npm run db:seed
4. Pull an Ollama model (optional)
ollama pull llama3.1:8b
5. Run the dev server
npm run dev
Open http://localhost:3000.
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
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-sessioncookies (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 pagescommunity— 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.
Notes on Android reuse
The API surface is plain JSON over HTTP with cookie-based auth. A React Native or Android client can reuse the same endpoints by:
- Implementing cookie jar storage for the
loam-sessioncookie - Calling
/api/auth/loginor/api/auth/registerto obtain a session - Reusing
/api/builds,/api/reviews,/api/ai/recommend, and/api/catalogas-is
The image composition endpoint (/api/images/compose) returns a PNG path; 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 |
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 |