Delete the environment variable example file. Add error handling to the Ollama AI route. Update Docker configuration to support Ollama services and create required image directories. Fix indentation in auth library and allow overriding secure cookie setting in non-production environments.
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
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 — recommended local model shown
OLLAMA_URL=http://localhost:11434
OLLAMA_MODEL=qwen3:32b
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 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
npm run dev
Open http://localhost:3000.
Docker
Build and run the web app plus PostgreSQL:
docker compose up --build
The app is exposed at 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:
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:
ollama pull qwen3:32b
If the app runs in Docker Compose and Ollama is bound to the host on Linux, set:
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-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.
Android client
The native Android app lives in android/. It uses the same plain JSON API over HTTP with cookie-based auth:
- 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
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:
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. |