Report road problems in Armenia: a map, AI photo classification, moderation and an open API.
Road traffic accidents are one of the leading causes of premature death in Armenia, and many of them start with a pothole, a missing sign or a broken traffic light that nobody reported to the people who can fix it. OpenRoad.am lets anyone with a phone take a photo, have it classified by AI, drop a pin on the map and send it to moderation. Approved reports are public: on the map, through a read-only REST API and through an MCP server, so citizens, journalists, municipalities and AI agents all work from the same data.
Production: https://open-roads-am-web.vercel.app (MVP; interface in Armenian, Russian and English)
| Landing | Map |
|---|---|
![]() |
![]() |
- A signed-in user uploads a photo. The API stores it in Cloudflare R2 and queues an AI classification job.
- A BullMQ worker sends the photo to Claude, which suggests a problem type (pothole, damaged sign, missing marking, ...) with a confidence score. The user confirms or changes it, pins the location and submits.
- Moderators review new reports; approved reports appear on the public map. Government agencies can move them to in progress and resolved with an API key.
- Everyone can read approved reports through the public API or the MCP server.
flowchart LR
user([Citizen / moderator]) --> web["apps/web<br/>Next.js 16 on Vercel"]
agent([AI agent: Claude, Cursor, ...]) --> mcp["apps/mcp-server<br/>MCP (stdio / HTTP)"]
web -->|REST, Clerk JWT| api["apps/api<br/>Fastify on Render"]
mcp -->|public REST / API key| api
api --> db[("PostgreSQL + PostGIS<br/>Supabase, via Prisma 7")]
api --> redis[("Redis<br/>Upstash")]
redis --> worker["BullMQ worker<br/>photo classification"]
worker --> claude["Claude API"]
api --> r2[("Cloudflare R2<br/>photos")]
worker --> r2
web -. auth .- clerk["Clerk"]
api -. auth / webhooks .- clerk
- Public API (
/api/v1/public/): read-only, no auth, rate limited by IP. - Internal API (
/api/v1/): Clerk JWT, rolesuser,moderator,gov_agency,admin. - AI classification is always asynchronous, so report creation never waits for the model.
The model is set by
CLASSIFY_MODEL(see Spec 03).
Turborepo + pnpm · TypeScript (strict) · Next.js 16, React 19, Tailwind CSS 4, MapLibre GL, next-intl · Fastify 5, Zod 4, Prisma 7, PostgreSQL + PostGIS, Redis, BullMQ · Clerk · Cloudflare R2 · Claude API · Model Context Protocol SDK · Vitest, Playwright · oxlint.
Details and rationale: TECH_STACK.md.
apps/
web/ Next.js 16 web app (map, report form, profile, moderation, admin)
api/ Fastify API, Prisma schema + migrations, BullMQ workers
mcp-server/ MCP server for external AI agents (talks to the API, never to the DB)
mobile/ Planned React Native + Expo app; currently an empty stub, not built or deployed
packages/
types/ Shared TypeScript types
config/ Shared tsconfig and oxlint config
ui/ Placeholder for shared UI components (empty for now)
specs/ Feature specs; every feature starts here (see CLAUDE.md)
docs/ Deployment guide, screenshots
Requirements: Node.js 22 (see .nvmrc), pnpm 9 (corepack enable), PostgreSQL with PostGIS, Redis.
pnpm install
# Local PostGIS and Redis, if you do not use Supabase / Upstash
docker run -d --name openroad-db -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=openroad -p 5432:5432 postgis/postgis:16-3.4
docker run -d --name openroad-redis -p 6379:6379 redis:7
# Environment
cp apps/api/.env.example apps/api/.env # every variable is validated with Zod at startup
cp apps/web/.env.example apps/web/.env # Clerk keys, NEXT_PUBLIC_API_BASE_URL=http://localhost:3001
# Database: generate the Prisma client and apply migrations
# (migrations also enable PostGIS and seed the problem types table)
pnpm --filter @open-road/api db:generate
pnpm --filter @open-road/api db:migrate
pnpm turbo dev --filter=@open-road/web --filter=@open-road/api # web on :3000, api on :3001You need your own Clerk application, R2 bucket and Claude API key; the step-by-step setup for every external
service is in docs/deployment.md. To run the MCP server against your API:
API_BASE_URL=http://localhost:3001 pnpm --filter @open-road/mcp-server dev.
pnpm lint # oxlint
pnpm typecheck # tsc --noEmit in every package
pnpm test # Vitest unit and integration tests (Prisma, Redis and Claude are mocked)
# End-to-end (Playwright, needs a Clerk test instance and seeded data; see specs/11-e2e-tests.md)
pnpm --filter @open-road/web test:e2eHusky runs lint-staged on commit and turbo typecheck test for affected packages on push. GitHub Actions runs
lint, typecheck and tests on every pull request, applies all migrations to a fresh PostGIS database, and can run the
Playwright suite nightly once the E2E secrets are configured.
specs/: one spec per feature (data model, report submission, AI classification, public API, moderation, auth, MCP server, ...). Implementation follows the spec.progress.md: what is actually implemented, updated with every change.docs/deployment.md: Clerk, Supabase, Upstash, R2, Render and Vercel setup.CLAUDE.md: development rules (branches, testing, security principles, locked product decisions).
MIT © 2026 Petr Grishin

