urRoute is a full-stack web application that connects bus operators (B2B) with travellers (B2C) through a gamified loyalty engine. Operators configure loyalty offers per level; travellers earn discounts and perks automatically with every booking they make.
- What This App Does
- Technology Stack
- Architecture Overview
- Folder Structure
- Database Schema
- Loyalty Engine
- Authentication & Security
- API Routes
- Background Jobs & Queues
- Local Setup — Step by Step
- Environment Variables Reference
- Useful Commands
- Design System & UI
- Search buses across multiple verified operators
- Select seats, enter passenger details, and pay via Razorpay
- Earn loyalty rewards (discounts, flat cashback, perks) per operator automatically
- Progress through 4 loyalty levels: Welcome → Stay → Loyalty → Champion
- View bookings, cancel trips, and check reward history
- Refer friends with a unique referral code to earn bonus points
- Register their fleet and get a dedicated multi-tenant workspace
- Create routes (origin → destination with boarding/dropping points)
- Schedule trips with seat maps, pricing, and amenities
- Configure loyalty offers per level (percentage discount, flat discount, group bonuses)
- View real-time dashboards: bookings, revenue, reward redemptions
- Approve or suspend operators
- View platform-wide analytics
- Monitor all bookings and revenue
| Layer | Technology | Why |
|---|---|---|
| Framework | Next.js 15 (App Router) | Full-stack with RSC + API routes in one repo |
| Language | TypeScript 5 | Type safety across frontend and backend |
| Database | PostgreSQL on Neon | Serverless-friendly, supports PgBouncer pooling |
| ORM | Prisma 6 | Schema-first, type-safe DB access |
| Auth | JWT (jose) + HTTP-only cookies | Access token (15m) + refresh token (14d) rotation |
| UI | shadcn/ui + Tailwind CSS | Accessible components with design token theming |
| Icons | Lucide React | Consistent icon set |
| Forms | React Hook Form + Zod | Schema-validated forms with TypeScript inference |
| Payments | Razorpay | Indian payment gateway with webhook verification |
| Resend + React Email | Transactional emails with JSX templates | |
| Push Notifications | Firebase Cloud Messaging | Mobile-style push in the browser |
| Background Jobs | BullMQ + Redis (ioredis) | Async notification and reward processing |
| Rate Limiting | Upstash Redis + @upstash/ratelimit | Serverless-compatible sliding window |
| Analytics | PostHog | Product analytics and feature flags |
| File Storage | Cloudinary | Operator logo uploads |
| State (client) | Zustand | Lightweight global state where needed |
| Data Fetching | TanStack Query | Cache, revalidation, and optimistic updates |
| Testing | Vitest | Fast unit tests (reward engine logic) |
| Theme | next-themes | Dark / light mode toggle |
| Logging | Winston | Structured server-side logs |
The app uses Clean Architecture layering inside a single Next.js monorepo:
HTTP Request
│
▼
API Route Handler (src/app/api/**/route.ts)
│ validates request with Zod, calls use-case
▼
Use Case (src/usecases/**)
│ orchestrates business logic, no framework code
▼
Repository (src/repositories/**)
│ talks only to Prisma; always filters by operatorId (multi-tenant)
▼
Prisma Client (src/lib/prisma.ts)
│
▼
PostgreSQL (Neon)
Every operator-owned database row has an operatorId column. The TenantRepository base class enforces that all queries always include a tenant filter — operators can never see each other's data. Travellers (Users) are global entities that ride many operators; their per-operator loyalty state lives in the RewardProgress table.
All monetary values are stored as integer paise (₹1 = 100 paise) to prevent floating-point drift. Field names always end in Minor (e.g. basePriceMinor, totalFareMinor). The UI divides by 100 to display rupee amounts.
- RSC (React Server Components): Data-fetching pages, dashboards, lists. They run on the server and have direct access to repositories.
- Client Components (
"use client"): Forms, interactive seat selection, payment flow, anything that needsuseStateoruseEffect.
urRoute/
├── prisma/
│ ├── schema.prisma # Single source of truth for the database
│ ├── migrations/ # Auto-generated SQL migration files
│ └── seed.ts # Seed script for local development data
│
├── src/
│ ├── app/ # Next.js App Router pages and API routes
│ │ ├── (auth)/ # Route group — login, register, forgot password
│ │ ├── (traveler)/ # Route group — traveller-facing pages (auth required)
│ │ │ ├── dashboard/ # Home dashboard with stats and upcoming trips
│ │ │ ├── search/ # Bus search with filters
│ │ │ ├── book/[tripId]/ # Seat selection and passenger form
│ │ │ ├── bookings/ # My bookings list + detail + payment
│ │ │ ├── rewards/ # Loyalty progress per operator
│ │ │ └── profile/ # Edit name, phone
│ │ ├── (operator)/ # Route group — operator portal (OPERATOR role)
│ │ │ └── operator/
│ │ │ ├── dashboard/ # Revenue and booking stats
│ │ │ ├── routes/ # Create and list routes
│ │ │ ├── trips/ # Schedule trips
│ │ │ ├── bookings/ # View all customer bookings
│ │ │ └── offers/ # Configure loyalty offers per level
│ │ ├── (admin-area)/ # Route group — admin panel (ADMIN role)
│ │ │ └── admin/
│ │ │ ├── page.tsx # Platform overview
│ │ │ ├── operators/ # Approve / suspend operators
│ │ │ └── analytics/ # Platform-wide revenue and usage
│ │ ├── api/ # All REST API endpoints
│ │ │ ├── auth/ # Login, register, refresh, logout, reset-password
│ │ │ ├── bookings/ # CRUD + cancel
│ │ │ ├── operators/ # Operator CRUD + sub-resources
│ │ │ ├── payments/ # Razorpay order creation + webhook
│ │ │ ├── trips/ # Trip search
│ │ │ ├── rewards/ # Progress and history
│ │ │ ├── profile/ # GET / PATCH current user
│ │ │ └── admin/ # Admin-only endpoints
│ │ ├── globals.css # Tailwind base + CSS design tokens
│ │ ├── layout.tsx # Root layout (ThemeProvider + Toaster)
│ │ └── page.tsx # Public landing page
│ │
│ ├── components/
│ │ ├── layout/
│ │ │ ├── traveler-nav.tsx # Dark navy sidebar for travellers
│ │ │ ├── operator-nav.tsx # Dark navy sidebar for operators
│ │ │ └── nav-user.tsx # User avatar dropdown (logout, profile)
│ │ ├── providers.tsx # next-themes ThemeProvider wrapper
│ │ └── ui/ # shadcn/ui component library
│ │ ├── button.tsx # Includes custom "action" (green) variant
│ │ ├── theme-toggle.tsx # Sun/moon dark-mode switch
│ │ └── ... # All other shadcn components
│ │
│ ├── lib/
│ │ ├── auth/
│ │ │ ├── session.ts # requireRole(), requireAuth(), getPrincipal() for RSC
│ │ │ ├── tokens.ts # JWT sign/verify with jose
│ │ │ ├── cookies.ts # HTTP-only cookie helpers
│ │ │ ├── hash.ts # SHA-256 for refresh token storage
│ │ │ └── password.ts # bcrypt wrapper
│ │ ├── reward-engine/
│ │ │ ├── index.ts # Main entry — computeDiscount(), advanceProgress()
│ │ │ ├── discount.ts # Discount calculation logic (%, flat, group bonus)
│ │ │ ├── progress.ts # Level advance and freeze logic
│ │ │ ├── types.ts # Shared types for the engine
│ │ │ └── __tests__/ # Vitest unit tests
│ │ ├── prisma.ts # Singleton Prisma client (avoids hot-reload leaks)
│ │ ├── redis.ts # ioredis client for BullMQ
│ │ ├── rate-limit.ts # Upstash rate limiter helpers
│ │ ├── http.ts # JSON response helpers (ok(), err())
│ │ ├── errors.ts # AppError class with HTTP status codes
│ │ └── logger.ts # Winston logger config
│ │
│ ├── repositories/ # Database access layer (Prisma queries)
│ │ ├── tenant.repository.ts # Base class enforcing operatorId filter
│ │ ├── user.repository.ts
│ │ ├── operator.repository.ts
│ │ ├── booking.repository.ts
│ │ ├── trip.repository.ts
│ │ ├── route.repository.ts
│ │ ├── offer-template.repository.ts
│ │ ├── reward-progress.repository.ts
│ │ ├── reward-history.repository.ts
│ │ ├── refresh-token.repository.ts
│ │ └── audit.repository.ts
│ │
│ ├── usecases/ # One file per business operation
│ │ ├── auth/ # Login, register, refresh, logout, reset-password
│ │ ├── bookings/ # Create, get, list, cancel
│ │ ├── payments/ # Create Razorpay order, verify signature
│ │ ├── rewards/ # Compute progress, freeze/unfreeze
│ │ ├── routes/ # CRUD routes
│ │ ├── trips/ # Create, search, update trips
│ │ ├── offers/ # Create and update offer templates
│ │ ├── operator/ # Register, approve, update operator
│ │ └── analytics/ # Pre-aggregate analytics
│ │
│ ├── dto/ # Data Transfer Objects (API response shapes)
│ │ ├── trip.dto.ts # TripDTO — fields the frontend can rely on
│ │ ├── booking.dto.ts
│ │ ├── operator.dto.ts
│ │ ├── reward.dto.ts
│ │ ├── route.dto.ts
│ │ └── user.dto.ts
│ │
│ ├── validators/ # Zod schemas for API request validation
│ │ ├── auth.ts
│ │ ├── booking.ts
│ │ ├── trip.ts
│ │ ├── route.ts
│ │ ├── operator.ts
│ │ └── offer-template.ts
│ │
│ ├── services/ # Thin wrappers for external services
│ │ ├── payment.service.ts # Razorpay order + verification
│ │ ├── notification.service.ts # Resend email + Firebase push
│ │ ├── token.service.ts # JWT lifecycle
│ │ └── audit.service.ts # AuditLog writes
│ │
│ ├── queues/
│ │ ├── index.ts # BullMQ queue definitions
│ │ ├── processors.ts # Job handlers (email, push, reward)
│ │ └── worker.ts # Worker process entry point
│ │
│ ├── emails/
│ │ └── booking-confirmation.tsx # React Email JSX template
│ │
│ ├── middleware.ts # Next.js edge middleware (JWT check, role guard)
│ ├── config/env.ts # Typed env variable access
│ ├── constants/auth.ts # Cookie names, token TTLs
│ └── types/auth.ts # JWTPayload, Principal interfaces
│
├── .env.example # Template for all required environment variables
├── tailwind.config.ts # Design tokens (brand, action, reward, sidebar)
├── next.config.ts
├── tsconfig.json
└── package.json
The database has 26 models. Here are the most important ones:
User— Global entity. One user can book with many operators. Has arole: ADMIN, OPERATOR, or TRAVELER. Has areferralCodefor the refer-and-earn system.Operator— The tenant (bus company). Linked 1-to-1 with its ownerUser. Every operator-owned table has anoperatorIdcolumn for isolation.
Route— A named path: origin → destination, with optional distance/duration and JSON arrays of boarding and dropping points.Trip— A scheduled departure on a route. HasbasePriceMinor(paise),departureAt,arrivalAt, seat layout, amenities.Seat— Individual seats for a trip.isBookedflips totruewhen a booking is confirmed. Labels follow the "L1", "U3" format (Lower/Upper deck, number).
Booking— Created when a traveller chooses seats. Starts asPENDING. Transitions toCONFIRMED(paid) →COMPLETED(trip done, triggers loyalty). Stores full fare breakdown:baseFareMinor,discountMinor,groupBonusMinor,taxMinor,totalFareMinor.Payment— One-to-one with Booking. Stores Razorpay order ID, payment ID, and verified signature. Status flows: CREATED → PAID → (optionally) REFUNDED.
OfferTemplate— Operator-configured reward per loyalty level. Defines discount type (PERCENTAGE or FLAT), the value, max cap, and group bonus rules. The reward engine reads these — it never hardcodes discount values.RewardProgress— Per(userId, operatorId)pair. TrackscurrentLevel,completedTrips, andstatus(ACTIVE or FROZEN). This is where the freeze/resume cycle lives.RewardHistory— Append-only ledger of every reward lifecycle event: UNLOCKED, REDEEMED, EXPIRED. Never updated, only inserted.
RefreshToken— Stored as SHA-256 hash. Uses afamilyIdsystem: reusing a revoked token nukes the whole family, preventing token theft replay attacks.AuditLog— Append-only record of every significant action (login, booking, offer update) with actor, IP, and metadata.AnalyticsDaily— Pre-aggregated per-operator daily stats. A cron job writes to this table; dashboards read from it instead of scanning the bookings table directly.
The loyalty engine lives in src/lib/reward-engine/. It is pure TypeScript with no side effects — no database calls, no API calls. This makes it fully unit-testable with Vitest.
| Level | Name | Trips Required | Typical Perk |
|---|---|---|---|
| L1 | Welcome | Start | Flat 11% off every ride |
| L2 | Stay | Trip 4 | 10% off + group bonuses |
| L3 | Loyalty | Trip 8 | ₹150 flat reward |
| L4 | Champion | Trip 12 | 15% off + priority perks |
After L4, the cycle continues back to L3 (never back to L1). Progress never fully resets — it only freezes.
If a traveller books with Operator A, their RewardProgress for Operator A is ACTIVE and completedTrips increments. The moment they book with Operator B, Operator A's progress is FROZEN (not deleted). When they return to Operator A, it resumes from exactly where it left off. This incentivises loyalty to a single operator.
POST /api/bookings
→ createBookingUsecase
→ reward-engine.computeDiscount(progress, offerTemplate, passengerCount)
→ if PERCENTAGE: discount = baseFare × (percentage / 100), capped at maxCap
→ if FLAT: discount = flatAmountMinor, capped at maxCap
→ groupBonus = (passengerCount - 1) × groupBonusPerHead, capped at groupBonusMaxHeads
→ totalFare = baseFare − discount − groupBonus + tax
→ booking created with full fare breakdown recorded
When a booking status changes to COMPLETED:
reward-engine.advanceProgress(currentProgress)
→ increment completedTrips
→ check if new total crosses the next level's threshold
→ if yes: update currentLevel, record RewardHistory(UNLOCKED)
→ if at L4 and trip count wraps: cycle back to L3, increment cycleCount
- Login — server issues an access token (15 min, signed with
JWT_ACCESS_SECRET) and a refresh token (14 days, signed withJWT_REFRESH_SECRET) - Both tokens are stored in HTTP-only, Secure, SameSite=Strict cookies — never accessible to JavaScript on the client
- When the access token expires, the client calls
POST /api/auth/refreshautomatically - The refresh token is stored in the database as a SHA-256 hash only — the plaintext never persists
- Every refresh rotates the token: old one is revoked, new one is issued. If a revoked token is reused (possible token theft), the entire token family is invalidated immediately
// In any RSC page — throws 401/403 and redirects if the role is wrong:
const principal = await requireRole("TRAVELER");
const principal = await requireRole("OPERATOR");
const principal = await requireRole("ADMIN");
const principal = await requireOperator(); // also checks operatorId exists on principalsrc/middleware.ts runs on every request at the CDN edge before the page renders. It reads the JWT cookie and redirects unauthenticated users to /login. Role-based path guards (e.g. /operator/* requires OPERATOR role) are also enforced here, before any server component runs.
Sensitive endpoints (login, register, password reset) use Upstash Redis sliding-window rate limiting. Exceeding the limit returns 429 Too Many Requests.
After Razorpay processes a payment, the client receives a razorpay_signature. The server re-computes HMAC-SHA256(orderId + "|" + paymentId, RAZORPAY_KEY_SECRET) and compares it against the received signature. The booking is only confirmed if the signatures match exactly.
All routes are under src/app/api/. They return { data: ... } on success or { error: { message, code } } on failure.
| Method | Path | Description |
|---|---|---|
| POST | /api/auth/register |
Create a TRAVELER account |
| POST | /api/auth/login |
Login and set JWT cookies |
| POST | /api/auth/logout |
Clear cookies and revoke refresh token |
| POST | /api/auth/refresh |
Rotate refresh token and issue new access token |
| POST | /api/auth/forgot-password |
Send password reset email |
| POST | /api/auth/reset-password |
Consume token and update password |
| GET | /api/auth/me |
Get current user profile |
| Method | Path | Description |
|---|---|---|
| GET | /api/trips?origin=&destination=&date= |
Search available trips |
| GET | /api/trips/[id] |
Get single trip with full seat map |
| Method | Path | Description |
|---|---|---|
| POST | /api/bookings |
Create booking (selects seats, computes fare with loyalty discount) |
| GET | /api/bookings |
List current user's bookings |
| GET | /api/bookings/[id] |
Get single booking with fare breakdown |
| POST | /api/bookings/[id]/cancel |
Cancel a PENDING or CONFIRMED booking |
| Method | Path | Description |
|---|---|---|
| POST | /api/payments |
Create a Razorpay order for a booking |
| POST | /api/payments/verify |
Verify Razorpay signature and confirm booking |
| POST | /api/payments/webhook |
Razorpay server-to-server event webhook |
| Method | Path | Description |
|---|---|---|
| POST | /api/operators |
Register a new operator |
| GET | /api/operators/[id] |
Get operator details |
| PATCH | /api/operators/[id] |
Update operator profile |
| GET / POST | /api/operators/[id]/routes |
List or create routes |
| GET / POST | /api/operators/[id]/trips |
List or create trips |
| GET / POST | /api/operators/[id]/offer-templates |
List or create loyalty offers |
| POST | /api/operators/[id]/approve |
Admin only — approve an operator |
| Method | Path | Description |
|---|---|---|
| GET | /api/rewards/progress |
My loyalty progress per operator |
| GET | /api/rewards/history |
My reward history ledger |
| Method | Path | Description |
|---|---|---|
| GET | /api/admin/operators |
List all operators (ADMIN role only) |
| GET | /api/admin/analytics |
Platform analytics (ADMIN role only) |
The worker runs as a separate process (pnpm worker) using BullMQ backed by Redis (ioredis).
| Queue | Jobs | Triggered When |
|---|---|---|
notifications |
Send email via Resend, send push via FCM | Booking confirmed, reward unlocked |
rewards |
Advance loyalty progress, freeze inactive tracks | Booking status changes to COMPLETED |
analytics |
Aggregate daily stats into AnalyticsDaily |
Cron job at midnight |
# In a second terminal alongside pnpm dev
pnpm workerThe worker is optional for basic UI development. It is required for emails, push notifications, and automated reward processing to fire.
Follow these steps exactly in order. Each step must succeed before moving to the next.
Make sure you have these installed on your machine:
# Node.js — must be version 18.17 or higher
node --version
# pnpm — if not installed, run: npm install -g pnpm
pnpm --version
# git
git --versionIf Node.js is too old, download and install the LTS version from nodejs.org.
git clone https://github.com/urRoute/Ur-Route-Web-App.git
# Move into the project folder
cd Ur-Route-Web-Apppnpm installThis downloads all packages into node_modules/. It may take 1–2 minutes the first time.
The app uses Neon — a free serverless PostgreSQL provider.
- Go to neon.tech and create a free account
- Click "New Project", give it any name (e.g.
urroute) - Once the project is created, click "Connect" or "Connection Details"
- You will see two connection strings:
- Pooled connection — has
pgbouncer=truein the URL (used at runtime) - Direct connection — no pgbouncer (used only for running migrations)
- Pooled connection — has
- Copy both connection strings — you will need them in Step 6
The app uses Upstash — a free serverless Redis provider.
- Go to upstash.com and create a free account
- Click "Create Database", select your nearest region, then click "Create"
- In your database page, open the "REST API" tab:
- Copy the UPSTASH_REDIS_REST_URL
- Copy the UPSTASH_REDIS_REST_TOKEN
- In the "Details" tab, copy the Redis connection string (starts with
rediss://) — this is used by the BullMQ worker
Copy the provided example:
cp .env.example .envOpen .env in your code editor and fill in each value:
# App
NODE_ENV=development
APP_URL=http://localhost:3000
# Database (Neon)
DATABASE_URL="postgresql://user:pass@ep-xxx.us-east-1.aws.neon.tech/neondb?sslmode=require&pgbouncer=true"
DIRECT_URL="postgresql://user:pass@ep-xxx.us-east-1.aws.neon.tech/neondb?sslmode=require"
# Auth — generate secrets in Step 7 below
JWT_ACCESS_SECRET="your-access-secret-here"
JWT_REFRESH_SECRET="your-refresh-secret-here"
JWT_ACCESS_TTL="900"
JWT_REFRESH_TTL="1209600"
# Redis (Upstash)
UPSTASH_REDIS_REST_URL="https://xxx.upstash.io"
UPSTASH_REDIS_REST_TOKEN="xxx"
REDIS_URL="rediss://default:xxx@xxx.upstash.io:6379"
# Payments (Razorpay) — use test keys from razorpay.com/dashboard
RAZORPAY_KEY_ID="rzp_test_xxxx"
RAZORPAY_KEY_SECRET="xxxx"
RAZORPAY_WEBHOOK_SECRET="xxxx"
# Email (Resend) — sign up at resend.com
RESEND_API_KEY="re_xxxx"
EMAIL_FROM="urRoute <noreply@yourdomain.com>"
# Optional — Firebase push notifications
FIREBASE_PROJECT_ID=""
FIREBASE_CLIENT_EMAIL=""
FIREBASE_PRIVATE_KEY=""
# Optional — Cloudinary file storage
CLOUDINARY_CLOUD_NAME=""
CLOUDINARY_API_KEY=""
CLOUDINARY_API_SECRET=""
# Optional — PostHog analytics
NEXT_PUBLIC_POSTHOG_KEY=""
NEXT_PUBLIC_POSTHOG_HOST="https://app.posthog.com"Important: Never commit
.envto git. It is already listed in.gitignore.
Run this command twice in your terminal. Each run gives you one unique secret string:
openssl rand -base64 48- Paste the first output as
JWT_ACCESS_SECRETin your.env - Paste the second output as
JWT_REFRESH_SECRETin your.env
These two values must be different from each other.
- Go to razorpay.com and create a free account
- In the dashboard, go to Settings → API Keys
- Click "Generate Test Mode API Keys"
- Copy the Key ID into
RAZORPAY_KEY_IDin your.env - Copy the Key Secret into
RAZORPAY_KEY_SECRETin your.env - For the webhook secret, you can use any random string for local development
- Go to resend.com and create a free account
- Go to API Keys and click "Create API Key"
- Copy the key into
RESEND_API_KEYin your.env - For
EMAIL_FROM, you can useurRoute <onboarding@resend.dev>for testing (Resend provides this default sender for free accounts)
This creates all 26 database tables in your Neon project:
pnpm prisma:migrateWhen it asks for a migration name, type init and press Enter.
You should see output ending with:
✔ Generated Prisma Client
✔ Applied 1 migration
If you see an SSL error, double-check your DATABASE_URL and DIRECT_URL in .env — make sure they both include sslmode=require.
pnpm prisma:generateThis generates TypeScript types from your schema so your code editor can autocomplete every database field.
pnpm db:seedThis inserts sample data so you can immediately explore all features:
| Role | Password | |
|---|---|---|
| Admin | admin@urroute.in |
Admin@1234 |
| Operator | operator@test.com |
Operator@1234 |
| Traveller | traveller@test.com |
Traveller@1234 |
It also creates 2 sample routes (Chennai → Coimbatore, etc.) and 5 scheduled trips.
pnpm devYou should see:
▲ Next.js 15.1.3
- Local: http://localhost:3000
✓ Ready in ~1s
Open http://localhost:3000 in your browser. The landing page should load immediately.
Open a second terminal in the same project folder and run:
pnpm workerThis is needed for:
- Booking confirmation emails (via Resend)
- Push notifications (via Firebase)
- Automatic loyalty reward processing after trips complete
You can skip this step if you only want to test the UI.
| What to check | How |
|---|---|
| Landing page loads | Open http://localhost:3000 |
| Registration works | Go to /register, create an account |
| Login works | Go to /login, use your credentials or seed data |
| Database connected | If login succeeds, the database connection is working |
| Search works | Go to /search, enter any city name |
| Seat selection works | Click "Book now" on any search result |
| Dark / light mode | Click the sun/moon icon in the sidebar header |
| Operator portal | Login as operator, go to /operator/dashboard |
| Admin panel | Login as admin, go to /admin |
Problem: Error: P1001 — Can't reach database server
Fix: Your DATABASE_URL or DIRECT_URL in .env is wrong. Go back to Neon, click Connect, and copy the exact strings again. Also check that your Neon project is not paused — free-tier projects pause after a period of inactivity. Opening the Neon dashboard wakes them up.
Problem: Invalid environment variables on startup
Fix: You have a missing or misspelled variable in your .env file. Compare your file line by line with .env.example to find what is missing.
Problem: Cannot find module '@/components/...'
Fix: Run pnpm install again. The @/ path alias maps to src/ and is configured in tsconfig.json. If that does not help, delete node_modules/ and .next/ then run pnpm install again.
Problem: Prisma error: The table 'public.users' does not exist
Fix: Your migrations have not been applied. Run pnpm prisma:migrate. If that fails with a connection error, check your DIRECT_URL in .env.
Problem: Login returns 401 even with the right password
Fix: Make sure JWT_ACCESS_SECRET and JWT_REFRESH_SECRET are set in .env and are not empty strings. Restart the dev server after editing .env.
Problem: Payments not working
Fix: Make sure you are using test mode keys from Razorpay (they start with rzp_test_). In test mode, Razorpay shows a fake payment screen — use card number 4111 1111 1111 1111, any future expiry date, and any CVV.
| Variable | Required | Description |
|---|---|---|
NODE_ENV |
Yes | development or production |
APP_URL |
Yes | Full URL of the app, e.g. http://localhost:3000 |
DATABASE_URL |
Yes | Neon pooled connection string (runtime queries) |
DIRECT_URL |
Yes | Neon direct connection string (migrations only) |
JWT_ACCESS_SECRET |
Yes | Secret for signing 15-minute access tokens |
JWT_REFRESH_SECRET |
Yes | Secret for signing 14-day refresh tokens |
JWT_ACCESS_TTL |
Yes | Access token lifetime in seconds — default 900 |
JWT_REFRESH_TTL |
Yes | Refresh token lifetime in seconds — default 1209600 |
UPSTASH_REDIS_REST_URL |
Yes | Upstash Redis REST endpoint (rate limiting) |
UPSTASH_REDIS_REST_TOKEN |
Yes | Upstash Redis REST token |
REDIS_URL |
Yes | Raw Redis connection string for BullMQ worker |
RAZORPAY_KEY_ID |
Yes | Razorpay publishable key (safe to show in browser) |
RAZORPAY_KEY_SECRET |
Yes | Razorpay secret key (server only, never expose) |
RAZORPAY_WEBHOOK_SECRET |
Yes | For verifying Razorpay webhook event signatures |
RESEND_API_KEY |
Yes | Resend API key for transactional emails |
EMAIL_FROM |
Yes | Sender name and address, e.g. urRoute <noreply@...> |
FIREBASE_PROJECT_ID |
No | Firebase project ID for push notifications |
FIREBASE_CLIENT_EMAIL |
No | Firebase service account email |
FIREBASE_PRIVATE_KEY |
No | Firebase service account private key |
CLOUDINARY_CLOUD_NAME |
No | Cloudinary cloud name for operator logo uploads |
CLOUDINARY_API_KEY |
No | Cloudinary API key |
CLOUDINARY_API_SECRET |
No | Cloudinary API secret |
NEXT_PUBLIC_POSTHOG_KEY |
No | PostHog project API key for analytics |
NEXT_PUBLIC_POSTHOG_HOST |
No | PostHog host, usually https://app.posthog.com |
# Start the development server
pnpm dev
# Check TypeScript types without building
pnpm typecheck
# Run unit tests (reward engine logic)
pnpm test
# Run tests in watch mode — re-runs when files change
pnpm test:watch
# Build for production
pnpm build
# Start the production server (run pnpm build first)
pnpm start
# Open Prisma Studio — a visual browser for your database
pnpm prisma:studio
# Create a new migration after editing schema.prisma
pnpm prisma:migrate
# Apply existing migrations without prompts (use in CI or production)
pnpm prisma:deploy
# Regenerate Prisma client after a schema change
pnpm prisma:generate
# Seed the database with test accounts and sample data
pnpm db:seed
# Start the BullMQ background worker (second terminal)
pnpm worker
# Format all files with Prettier
pnpm format
# Run ESLint
pnpm lintThe entire colour system is driven by CSS custom properties defined in src/app/globals.css and mapped to Tailwind utility classes in tailwind.config.ts.
| Token | Light Mode Value | Usage |
|---|---|---|
--primary |
hsl(228, 62%, 26%) deep navy |
Logo, links, structural elements |
--action |
hsl(134, 62%, 41%) vivid green |
All CTA buttons: Search, Book, Pay |
--reward |
hsl(38, 92%, 52%) warm gold |
Loyalty coins, points, reward badges |
--sidebar |
hsl(228, 68%, 16%) dark navy |
Sidebar background — always dark in both themes |
The app uses next-themes. Dark mode works by toggling the dark class on the <html> element — Tailwind's dark: variants respond to it. The user's chosen theme is saved automatically in localStorage.
The sun/moon toggle button (src/components/ui/theme-toggle.tsx) appears in the top-right corner of every sidebar. The main content area switches between light and dark. The sidebar always stays dark navy regardless of the chosen theme.
The Button component (src/components/ui/button.tsx) has all standard shadcn variants plus one custom one:
| Variant | Appearance | When to use |
|---|---|---|
default |
Navy blue | Secondary actions |
action |
Green | Primary CTAs — Search, Book, Pay, Proceed |
outline |
Bordered, transparent | Ghost actions on light backgrounds |
ghost |
No background | Toolbar buttons, links |
destructive |
Red | Cancel, delete |
Next.js route groups (folders in parentheses) share layouts without affecting URLs:
| Folder | Resolves to URLs | Who accesses it |
|---|---|---|
(auth) |
/login, /register, /forgot-password |
Public — no login required |
(traveler) |
/dashboard, /search, /bookings, /rewards, /profile |
Logged-in TRAVELER role |
(operator) |
/operator/* |
Logged-in OPERATOR role |
(admin-area) |
/admin, /admin/operators, /admin/analytics |
ADMIN role only |
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature-name - Make your changes
- Run
pnpm typecheckandpnpm test— both must pass with zero errors - Commit with a descriptive message
- Push and open a Pull Request against
main
MIT — free to use, modify, and distribute.