OBS EXPERIENCES

Oh Bhaisahab — MVP Technical Architecture & Cost Estimation
Extends MVP Architecture v1.0 | June 2026

1. Backend Architecture Design

The OBS MVP backend is a monolithic Node.js/Express API deployed in Docker, backed by a single PostgreSQL database. This approach minimises operational complexity for an early-stage product while laying the groundwork for future service extraction.

1.1 Technology Choices

Component Technology Rationale
API Server Node.js 20 LTS + Express 5 Team familiarity; excellent async I/O for real-time features
Database PostgreSQL 16 Relational integrity for bookings; JSONB for flexible trek metadata
ORM Prisma ORM Type-safe queries; auto-generated migrations; clean schema file
Auth JWT (access) + Refresh tokens Stateless; works offline-first for mobile
File Storage AWS S3 / Cloudflare R2 Trek media, user documents, certificates
Email Resend (or AWS SES) Transactional emails: booking confirmation, OTP, receipts
SMS / OTP MSG91 or Twilio India OTP verification; WhatsApp booking confirmations
Payments Razorpay India-first; supports UPI, cards, EMI, net banking
Background Jobs BullMQ + Redis Scheduled reminders, email queues, certificate generation
Caching Redis Session store, rate limiting, frequently-read trek data
Containerisation Docker + Docker Compose Consistent dev/prod parity; easy VPS deployment

1.2 Database Schema (Core Tables)

All tables include created_at, updated_at timestamps and soft-delete via deleted_at where applicable.

Users & Auth

Table Key Columns Notes
users id (uuid PK), email, phone, name, profile_photo_url, bio, referral_code, referred_by_id Core identity table
auth_tokens id, user_id (FK), refresh_token_hash, device_info, expires_at, revoked_at Refresh token store; one row per device session
otps id, phone/email, otp_hash, purpose, expires_at, verified_at Purpose: login | signup | password_reset
emergency_contacts id, user_id (FK), name, relationship, phone, email, is_primary Min 2 required before trek activation

Treks & Inventory

Table Key Columns Notes
treks id, slug, name, description, region, difficulty (enum), max_altitude_m, duration_days, min_group_size, max_group_size, status (draft|active|archived), meta (JSONB) Master trek catalogue
trek_media id, trek_id (FK), url, type (image|video), sort_order, is_hero S3 keys for trek imagery/video
trek_itinerary_days id, trek_id (FK), day_number, title, camp_name, altitude_m, distance_km, terrain_type, highlights, meal_plan Day-by-day breakdown
trek_batches id, trek_id (FK), start_date, end_date, price_inr, seats_total, seats_booked, seats_held, guide_id (FK), status (open|full|cancelled) Each scheduled departure
guides id, name, bio, photo_url, years_experience, phone, is_active Guide roster; linked to batches
faqs id, entity_type (trek|global), entity_id, question, answer, sort_order Trek-specific and global FAQs

Bookings & Payments

Table Key Columns Notes
bookings id, user_id (FK), batch_id (FK), status (pending|confirmed|cancelled|completed), participants_count, total_amount_inr, paid_amount_inr, coupon_id (FK), cancellation_reason, cancelled_at Central booking record
booking_participants id, booking_id (FK), name, age, id_proof_url, is_primary Each trekker on a booking
payments id, booking_id (FK), razorpay_order_id, razorpay_payment_id, amount_inr, status (created|captured|failed|refunded), payment_method, captured_at Payment ledger; immutable append-only
coupons id, code (unique), discount_type (pct|flat), discount_value, min_order_inr, max_uses, used_count, valid_from, valid_until, is_active Promo/discount codes
installment_plans id, booking_id (FK), installment_number, due_date, amount_inr, paid_at, payment_id (FK) EMI / part-payment tracking

Documents & Pre-Trek

Table Key Columns Notes
user_documents id, user_id (FK), booking_id (FK), doc_type (id_proof|fitness_form|waiver|medical), s3_key, original_filename, status (pending|approved|rejected), reviewed_by Uploaded pre-trek docs
packing_checklist_items id, trek_id (FK nullable), name, category, is_mandatory, sort_order Default items; users can add custom items
user_checklist_progress id, user_id (FK), booking_id (FK), item_id (FK), is_checked Per-booking checklist state

Loyalty & Referrals

Table Key Columns Notes
loyalty_points id, user_id (FK), points_delta (can be negative), reason, reference_id, reference_type, balance_after Append-only ledger; never update rows
loyalty_tiers id, name, min_points, max_points, discount_pct, perks (JSONB) Newcomer → Trekker → Explorer → Summit Seeker → OBS Legend
referrals id, referrer_id (FK), referee_id (FK), booking_id (FK nullable), status (registered|booking_confirmed), points_awarded_at Referral tracking

Support

Table Key Columns Notes
support_tickets id, user_id (FK), booking_id (FK nullable), subject, status (open|in_progress|resolved|closed), priority Customer support tickets
support_messages id, ticket_id (FK), sender_type (user|admin), message, attachments (JSONB) Thread messages on a ticket
faqs id, entity_type, entity_id, question, answer, sort_order Static FAQ content (also covers global)

1.3 API Route Structure

RESTful API with /api/v1 prefix. JWT Bearer token required on all protected routes (marked *).

Auth Routes (/api/v1/auth)

Method Path Description
POST /send-otp Send OTP to phone/email
POST /verify-otp Verify OTP → return access + refresh tokens
POST /login Email + password login (fallback)
POST /register Email/social registration
POST /refresh Rotate refresh token
POST /logout * Revoke current device refresh token
POST /logout-all * Revoke all refresh tokens (user account)

Trek Routes (/api/v1/treks)

Method Path Description
GET / List all active treks (filters: region, difficulty, duration, price, month)
GET /:slug Trek detail page data (includes itinerary, media, batches, guides, FAQs)
GET /:id/batches Available batches with seat counts and prices
GET /:id/reviews Paginated verified reviews for a trek
GET /:id/itinerary/pdf * Generate + return offline itinerary PDF
GET /:id/packing-list Default packing list items for a trek

Booking Routes (/api/v1/bookings)

Method Path Description
POST / * Create booking (reserve seats, create Razorpay order)
GET / * List user's bookings (status filter)
GET /:id * Booking detail (itinerary, documents, payments, checklist)
POST /:id/cancel * Cancel booking (triggers refund calculation)
POST /:id/participants * Add/update participant details
POST /:id/documents * Upload pre-trek document (presigned S3 URL flow)
GET /:id/documents * List documents for a booking
PATCH /:id/checklist * Update packing checklist item state

Payment Routes (/api/v1/payments)

Method Path Description
POST /webhook Razorpay webhook handler (HMAC verified, public endpoint)
POST /verify * Client-side payment capture verification
GET /booking/:bookingId * Payment history for a booking

User Routes (/api/v1/users)

Method Path Description
GET /me * Current user profile + loyalty tier
PATCH /me * Update profile (name, bio, photo)
POST /me/photo * Upload profile photo (presigned S3 flow)
GET /me/emergency-contacts * List emergency contacts
POST /me/emergency-contacts * Add emergency contact
DELETE /me/emergency-contacts/:id * Remove emergency contact
GET /me/loyalty * Points balance, tier, history
GET /me/referrals * Referral stats and tracking
DELETE /me * GDPR data deletion request

Misc Routes

Method Path Description
GET /api/v1/faqs Global FAQs (also trek-specific via ?trek_id=)
POST /api/v1/support/tickets * Create support ticket
GET /api/v1/support/tickets * User's support tickets
POST /api/v1/support/tickets/:id/messages * Reply to ticket
GET /api/v1/coupons/validate * Validate a coupon code against a batch

1.4 Admin Routes (/api/v1/admin)

All admin routes require admin JWT (separate role claim). Admin panel is a separate Next.js app consuming these.

Domain Routes Summary
Treks CMS CRUD treks, itinerary days, media upload, FAQ management
Batch Management Create/edit/cancel batches; assign guides; adjust capacity
Bookings View all bookings; manual confirmation; refund initiation; export CSV
Users View users; adjust loyalty points; support escalations
Documents Review uploaded participant docs; approve/reject with notes
Analytics Revenue by period, booking conversion, trek popularity, coupon usage
Coupons Create/deactivate coupon codes; usage reports
Guides CRUD guide profiles; trek assignment history

1.5 Key Backend Flows

Booking Flow

Step Action Notes
1 Client calls POST /bookings with batch_id, participants_count, coupon_code Server validates seat availability using database row-level lock
2 Server reserves seats (seats_held++) and creates Razorpay order Hold expires in 15 mins if payment not completed
3 Client completes Razorpay checkout, receives payment_id Happens client-side in Razorpay SDK
4 Client calls POST /payments/verify with razorpay signature Server validates HMAC signature against Razorpay secret
5 Server confirms booking, converts held seats to booked Sends confirmation email via Resend + SMS via MSG91
6 BullMQ job enqueued: generate certificate, schedule reminders Async - does not block the API response

Referral Flow

Step Action
1 User A shares referral link containing their referral_code
2 User B registers with the referral code → referrals row created (status: registered)
3 User B completes their first booking → referrals status → booking_confirmed
4 BullMQ job awards ₹500 equivalent points to User A (configurable); sends notification

1.6 Security Considerations

  • Rate limiting via Redis on all auth endpoints (5 OTP attempts per 10 minutes per phone)
  • Razorpay webhook signature verification (HMAC-SHA256) before any payment state change
  • S3 document uploads use presigned URLs — backend never streams files through the API server
  • All user-uploaded content scanned for malware via ClamAV or AWS Macie before approval
  • Row-level security on booking queries — users can only access their own bookings
  • Admin routes protected by separate role claim in JWT; admin panel on separate subdomain
  • HTTPS enforced everywhere; HSTS headers on website and API
  • Database credentials, JWT secrets, Razorpay keys stored in environment variables; never in code

2. Website Sitemap (Next.js)

The website is the primary discovery and booking channel. Built with Next.js App Router using SSG for trek detail pages (fast load, SEO-optimised) and RSC+Client components for interactive booking flows.

2.1 Public Pages (No Auth Required)

Route Page Name Rendering Key Components
/ Home / Landing SSG + ISR (1hr) Hero video, featured treks carousel, 'Why OBS' section, testimonials, CTA band
/treks Trek Listing SSR (filters) Search bar, filter panel (region/difficulty/duration/price), trek cards grid, 'no results' state
/treks/[slug] Trek Detail SSG + ISR (1hr) Hero media, quick facts bar, day-wise itinerary accordion, batch selector, guide profiles, reviews, packing list, weather widget, FAQ accordion, sticky Book Now CTA
/blog Blog Index SSG + ISR Article cards, category filters, search
/blog/[slug] Blog Post SSG + ISR Article content, related treks, author card, social share
/about About OBS SSG Team, story, values, mission
/guides Our Guides SSG + ISR Guide profiles grid with experience badges
/faq FAQ SSG + ISR Accordion FAQ, search within FAQ, contact CTA
/contact Contact Us SSG Contact form, WhatsApp link, office address, support hours
/terms Terms & Conditions SSG Static legal content
/privacy Privacy Policy SSG Static legal content
/cancellation Cancellation Policy SSG Policy table, refund timeline
/404 Not Found Static Trek suggestions, home link

2.2 Auth Pages

Route Page Name Notes
/login Login / Sign Up Unified auth page; tab between login / register; Google/Apple OAuth; phone OTP
/auth/callback OAuth Callback Server-side token exchange; redirect to /dashboard or intended page
/auth/verify-otp OTP Verification Phone OTP entry; resend timer; back link
/auth/forgot-password Forgot Password Email input → magic link or OTP reset flow

2.3 Booking Flow (Auth Required)

Route Step Notes
/book/[batchId] Step 1 — Participants Participant count selector, primary trekker details, emergency contact required
/book/[batchId]/details Step 2 — Trek Details Review Itinerary summary, what's included, cancellation policy acknowledgement
/book/[batchId]/payment Step 3 — Payment Order summary, coupon code input, Razorpay checkout widget, loyalty points redemption
/book/[batchId]/confirm Step 4 — Confirmation Booking ID, PDF download, next steps checklist, share card

2.4 User Dashboard (Auth Required — /dashboard)

Route Page Name Notes
/dashboard Dashboard Home Upcoming trek card, quick links, loyalty tier progress, recent activity
/dashboard/bookings My Bookings Tabs: Upcoming / Past / Cancelled; booking cards with status badges
/dashboard/bookings/[id] Booking Detail Trek info, participants, payment history, document status, packing checklist, group details
/dashboard/bookings/[id]/documents Documents Upload Upload ID proof, fitness form, waiver; document status tracker
/dashboard/profile My Profile Photo upload, bio, edit personal details, emergency contacts manager
/dashboard/loyalty Loyalty & Rewards Points balance, tier badge, history ledger, redeem options
/dashboard/referrals Refer & Earn Referral code card, share options, tracking table (invited/registered/booked), earnings summary
/dashboard/support Support Create ticket, ticket list with status, message thread view
/dashboard/settings Settings Notification preferences, language, privacy, account deletion

2.5 Admin Panel (Separate Next.js App — admin.ohbhaisahab.com)

Route Section Key Functionality
/admin/dashboard Overview Revenue today/week/month, bookings pending, seats available, open support tickets
/admin/treks Trek CMS List + create/edit treks; itinerary editor; media uploader; FAQ editor
/admin/batches Batch Management Calendar view + list view; create batch; assign guide; capacity management
/admin/bookings Bookings All bookings table with filters; booking detail; manual actions; CSV export
/admin/users Users User list; profile view; loyalty adjustment; flag/ban
/admin/documents Document Review Pending documents queue; approve/reject with notes
/admin/guides Guides Guide CRUD; assignment history
/admin/coupons Coupons Create/deactivate codes; usage analytics
/admin/support Support Tickets Ticket queue; assign to agent; respond; resolve
/admin/analytics Analytics Revenue charts, trek performance, conversion funnel, cohort retention

3. Mobile App Sitemap (React Native Expo)

The Expo app uses Expo Router (file-based routing) with a 5-tab bottom navigation structure as defined in the PRD. Authentication state is managed via Zustand + Secure Store for JWT persistence.

3.1 Unauthenticated Stack

Screen Route / Component Description
Splash / Cinematic Entry app/index.tsx (redirects) Himalayan video loop; logo fade-in; tagline animation; single Begin CTA
Welcome app/(auth)/welcome.tsx Two paths: 'New here' → sign-up | 'Trekked with OBS' → login
Login app/(auth)/login.tsx Phone OTP (primary), Google OAuth, Apple OAuth, Email+Password
OTP Verify app/(auth)/verify-otp.tsx 6-digit OTP input; 60s resend timer; auto-submit on fill
Sign Up app/(auth)/register.tsx Name, email, phone; referral code (optional); tshirt size (optional)
Forgot Password app/(auth)/forgot-password.tsx Email input → OTP reset flow
Guest Mode app/(auth)/guest.tsx Limited explore access; booking gates to auth prompt

3.2 Main App — Bottom Tab Navigator

Tabs: Explore | My Treks | Profile (MVP scope — Tribe and Memories are post-MVP)

Tab 1: Explore

Screen Route Description
Explore Home app/(tabs)/explore/index.tsx Featured trek cards, region quick-filters, upcoming batch highlights, search bar
Search / Filter app/(tabs)/explore/search.tsx Full-text search; filters (region, difficulty, duration, price, month); sort options
Trek Detail app/(tabs)/explore/trek/[slug].tsx Hero media carousel, quick facts, itinerary accordion, batch selector, guides, reviews, FAQs, Book Now CTA
Batch Selector app/(tabs)/explore/trek/[slug]/batches.tsx Calendar month view + list; seat badges; price per batch; Select CTA
Guide Profile app/(tabs)/explore/guide/[id].tsx Photo, bio, experience stats, past trek reviews
FAQ Detail app/(tabs)/explore/faq.tsx Global FAQ accordion; search within FAQ
Blog List app/(tabs)/explore/blog/index.tsx Article cards with category tags
Blog Post app/(tabs)/explore/blog/[slug].tsx Article content, related treks

Tab 2: My Treks

Screen Route Description
My Treks Home app/(tabs)/my-treks/index.tsx Upcoming trek hero card with countdown; past treks list; empty state with Explore CTA
Booking Detail app/(tabs)/my-treks/booking/[id].tsx Trek summary, participants, payment status, documents checklist, packing list, guide contact
Itinerary Viewer app/(tabs)/my-treks/booking/[id]/itinerary.tsx Day-by-day view with altitude, meals, terrain; elevation profile chart; offline-cached PDF download
Packing Checklist app/(tabs)/my-treks/booking/[id]/checklist.tsx Categorised checklist with checkboxes; custom item add; progress bar
Documents app/(tabs)/my-treks/booking/[id]/documents.tsx Upload ID proof, fitness form, waiver; status indicators (pending / approved / rejected)
Payment History app/(tabs)/my-treks/booking/[id]/payments.tsx Timeline of payments, receipts download, remaining balance
Past Trek Archive app/(tabs)/my-treks/history.tsx Timeline of completed treks; stats summary (total altitude, km, camps)

Tab 3: Profile

Screen Route Description
Profile Home app/(tabs)/profile/index.tsx Photo, name, bio, trek stats summary, loyalty tier badge, quick links
Edit Profile app/(tabs)/profile/edit.tsx Edit name, bio, photo upload, trek preferences
Loyalty & Rewards app/(tabs)/profile/loyalty.tsx Points balance, tier progress bar, points history ledger, redeem options
Referral Hub app/(tabs)/profile/referrals.tsx Unique code card with share sheet; tracking: invited / registered / booked; earnings
Emergency Contacts app/(tabs)/profile/emergency-contacts.tsx List contacts; add/edit/delete; minimum 2 required indicator
Support app/(tabs)/profile/support/index.tsx Ticket list with status tabs; Create new ticket button
Support Ticket app/(tabs)/profile/support/[id].tsx Message thread; attach screenshot; mark resolved
Settings app/(tabs)/profile/settings.tsx Notification preferences, language, privacy settings, offline data, delete account
Notification Prefs app/(tabs)/profile/settings/notifications.tsx Granular toggles: booking reminders, trek updates, promotional, community

3.3 Booking Flow (Modal Stack — overlays any tab)

Screen Route Description
Booking Start app/book/[batchId]/index.tsx (modal) Participant count stepper; primary trekker details; price breakdown
Participant Details app/book/[batchId]/participants.tsx Additional participant names, ages; emergency contact confirmation
Review & Coupon app/book/[batchId]/review.tsx Order summary, coupon code input, loyalty points toggle, policy acknowledgement
Payment app/book/[batchId]/payment.tsx Razorpay SDK checkout; loading state; retry on failure
Confirmation app/book/[batchId]/confirm.tsx Success animation, booking ID, share card, 'Go to My Treks' CTA

3.4 Offline Strategy

  • Itinerary PDFs are auto-cached to device storage 48 hours before trek start date (background fetch)
  • Packing checklist state is persisted locally via SQLite (expo-sqlite); synced to server when online
  • Trek detail page hero images and essential content cached via react-query persistent cache
  • Offline indicator banner shown when network is unavailable; core app features remain usable
  • Document upload queued locally when offline; auto-uploads when connectivity restored

4. Cloud & Infrastructure Cost Estimation

Estimates are in Indian Rupees (INR) per month at MVP scale (0–500 bookings/month, ~2,000 active users). Costs scale with usage — this is a conservative early-stage baseline. 1 USD ≈ ₹84.

4.1 Cloud Hosting — Backend API & Database

Service Provider Spec / Plan Cost/Month (INR) Notes
App Server (API) Hetzner VPS (CX21) or DigitalOcean 2 vCPU, 4GB RAM, 40GB SSD ₹1,680 – ₹2,100 Single Docker host; ideal for MVP; scale to CX31 at growth
PostgreSQL DB Same VPS (self-managed) or Supabase Free → Pro Self: included above | Supabase Pro: 8GB storage ₹0 – ₹3,360 Self-managed on VPS saves cost; Supabase adds managed backups & PgBouncer
Redis Cache + BullMQ Same VPS (Docker container) ~256MB RAM allocation ₹0 (included in VPS) Upstash Redis ₹0 free tier for low throughput if preferred
Backup Storage Hetzner Backup or S3 Automated daily DB backups, 7-day retention ₹420 – ₹840 Non-negotiable; automate with pg_dump + rclone to S3
Load Balancer / Proxy Nginx on VPS (self-managed) Reverse proxy + SSL termination ₹0 Use Caddy for auto-HTTPS simplicity
Managed Hosting Alt. Railway.app (Starter Plan) 1GB RAM, Postgres included, auto-deploy ₹1,680 – ₹5,040 Best DX for small team; auto-scaling; slightly higher cost
Backend Subtotal ₹2,100 – ₹8,400/mo

4.2 Website Hosting (Next.js)

Service Provider Plan Cost/Month (INR) Notes
Next.js Website Vercel (Hobby → Pro) Hobby: Free | Pro: $20/mo ₹0 – ₹1,680 Hobby sufficient for early MVP; Pro needed at traffic scale or for teams
Admin Panel (Next.js) Vercel Hobby or same VPS Hobby: Free ₹0 Low traffic; free tier handles it easily
CDN (Static Assets) Cloudflare Free Unlimited bandwidth, DDoS protection ₹0 Point DNS to Cloudflare; instant free CDN globally
Domain (.com) Namecheap / GoDaddy ohbhaisahab.com annual ₹168/mo (₹2,000/yr) Already owned; renewal cost only
Website Subtotal ₹168 – ₹1,848/mo

4.3 File Storage (Trek Media + User Documents)

Service Provider Plan Cost/Month (INR) Notes
Object Storage Cloudflare R2 10GB free, $0.015/GB/mo after ₹0 – ₹840 Zero egress fees — critical for serving trek images to users
Image CDN / Transform Cloudflare Images or imgix CF Images: $5/mo for 100k images ₹420 – ₹840 Resize + optimise trek imagery on the fly; massive performance win
Document Storage Cloudflare R2 (same bucket) User ID proofs, fitness forms; separate private bucket ₹0 – ₹420 Private bucket; access via presigned URLs only
Storage Subtotal ₹420 – ₹2,100/mo

4.4 Communication Services

Service Provider Plan / Volume Cost/Month (INR) Notes
Transactional Email Resend Free: 3,000 emails/mo | Pro: $20 for 50k ₹0 – ₹1,680 Booking confirmations, receipts, reminders; excellent deliverability
SMS & OTP MSG91 (India) ~₹0.18 per OTP SMS; 500 OTPs/mo ₹500 – ₹1,500 India-optimised; supports DLT registration required for commercial SMS
WhatsApp (Booking Updates) MSG91 / Interakt ~₹0.58/conversation (business-initiated) ₹500 – ₹2,000 Optional at MVP; high open rate; DLT + Meta approval required
Push Notifications Expo Push Notifications (FCM/APNs) Free (Expo managed) ₹0 Expo handles FCM/APNs routing; no separate vendor needed
Communications Subtotal ₹1,000 – ₹5,180/mo

4.5 Payment Processing

Service Provider Fee Structure Estimated Monthly Cost Notes
Payment Gateway Razorpay 2% per transaction (domestic cards/UPI) Variable — 2% of GMV No monthly fee; purely transaction-based. On ₹5L GMV = ₹10,000
Razorpay Account Razorpay Free account activation ₹0 KYC and bank account linking required; takes 2–3 business days
International Cards Razorpay International 3% per transaction Variable Only needed if targeting NRI trekkers

4.6 Developer Accounts (One-Time Costs)

Account Platform Cost Notes
Google Play Developer Google Play Console $25 one-time (≈ ₹2,100) Lifetime account; covers all Android apps
Apple Developer Program App Store Connect $99/year (≈ ₹8,316/year) Annual renewal required; needed for iOS TestFlight + App Store
Expo Application Services (EAS) Expo Free tier: limited builds | Production plan: $99/mo EAS Build for OTA updates and app store submissions; free tier sufficient at MVP

4.7 Monitoring & DevOps

Service Provider Plan Cost/Month (INR) Notes
Error Tracking Sentry Free: 5k errors/mo | Team: $26/mo ₹0 – ₹2,184 Free tier sufficient at MVP; essential for production debugging
Uptime Monitoring Better Uptime / UptimeRobot Free tier (5-min checks) ₹0 Alert on API or website downtime
Logging Self-hosted (Docker logs + Loki) On VPS (included) ₹0 Or Papertrail free tier for hosted log management
Analytics (Web) Vercel Analytics or Plausible Vercel: included in Pro | Plausible: $9/mo ₹0 – ₹756 Privacy-friendly; no cookie consent required with Plausible
CI/CD GitHub Actions Free for public repos; 2000 min/mo for private ₹0 Auto-deploy to VPS on push to main; Expo EAS build on PR
DevOps Subtotal ₹0 – ₹2,940/mo

4.8 Total Monthly Cost Summary

Category Low Estimate (INR/mo) High Estimate (INR/mo) Notes
Backend Hosting (VPS + DB) ₹2,100 ₹8,400 Lower = Hetzner self-managed; Higher = Railway managed
Website Hosting (Vercel) ₹168 ₹1,848 Lower = Hobby + domain; Higher = Pro plan
File Storage (R2 + CDN) ₹420 ₹2,100 Scales with trek media volume
Communications (Email + SMS) ₹1,000 ₹5,180 SMS cost grows with user registrations
Payment Gateway 2% of GMV 2% of GMV Variable; not in fixed total
Monitoring & DevOps ₹0 ₹2,940 Sentry paid + Plausible analytics
TOTAL (Fixed) ≈ ₹3,700/mo ≈ ₹20,468/mo Excluding payment gateway %
Recommended Budget ₹6,000 – ₹10,000/mo Realistic mid-range for a lean MVP team at launch

4.9 One-Time Setup Costs

Item Cost (INR) Notes
Google Play Developer Account ₹2,100 (one-time) Permanent
Apple Developer Program (Year 1) ₹8,316/year Renew annually at ₹8,316
DLT Registration (SMS) ₹1,000 – ₹2,000 Required by TRAI for commercial SMS in India; one-time with telecom
Domain (if not already owned) ₹800 – ₹2,000/year ohbhaisahab.com
SSL Certificate ₹0 Use Let's Encrypt via Caddy/Certbot — free automated
Initial VPS Setup & Hardening One-time effort (engineer hours) SSH keys, firewall, Docker, Nginx/Caddy, automated backups
TOTAL One-Time ≈ ₹11,400 – ₹14,400 Excluding engineer time

4.10 Cost Scaling Guidance

  • At 500 bookings/month (≈ ₹25L GMV): payment gateway fees ≈ ₹50,000; infrastructure stays in low range
  • At 2,000 bookings/month: upgrade VPS to 4 vCPU / 8GB RAM (≈ +₹1,680/mo); consider managed Postgres (≈ +₹3,360/mo)
  • At 10,000+ users: add Redis Cluster, CDN caching layer, consider Kubernetes or ECS; budget ₹50,000+/mo
  • Mobile app OTA updates via Expo EAS Update are free on hobby tier; upgrade to production plan ($99/mo) at scale
  • WhatsApp Business API becomes cost-effective over SMS above ₹100/customer LTV — factor in at Phase 2

OBS EXPERIENCES

Addendum: Trek Batch Roles, Live Status & Loyalty

Extends MVP Architecture v1.0 | June 2026

1. Overview & Design Principles

Extends MVP Architecture v1.0 | Added June 2026

This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.

Three principles guide the design:

  • Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
  • Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
  • Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.

2. Database Schema Changes

2.1 Updated trek_batches table

Existing columns carry forward. New and changed columns are marked.

Column Type Default Notes
id uuid PK gen_random_uuid() No change
trek_id uuid FK → treks No change
start_date date No change
end_date date No change
price_inr integer No change
seats_total integer No change
seats_booked integer 0 No change
seats_held integer 0 No change
status trek_batch_status (enum) 'scheduled' NEW — see lifecycle §3
status_updated_at timestamptz now() NEW — when status last changed
status_updated_by uuid FK → users null NEW — leader who changed it
started_at timestamptz null NEW — set when status → in_progress
finished_at timestamptz null NEW — set when status → completed
actual_end_date date null NEW — may differ from planned end_date
incident_notes text null NEW — free text, leader-only write
cancellation_reason text null No change
meeting_point text null NEW — e.g. 'Sankri base camp parking'
meeting_time timestamptz null NEW — assembly time day 1
💡
Add a Postgres enum: CREATE TYPE trek_batch_status AS ENUM ('scheduled','assembling','in_progress','paused','completed','cancelled');

2.2 New table: batch_staff

Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE
user_id uuid FK → users Staff must have a user account
role batch_staff_role (enum) trek_leader | guide | coordinator — see §2.3
assigned_by uuid FK → users Admin who made the assignment
assigned_at timestamptz Timestamp of assignment
is_active boolean true = currently on this batch; false = removed/replaced
notes text Internal notes (e.g. 'backup leader', 'local area expert')
💡
Unique constraint: (batch_id, user_id, role) — same person cannot hold the same role twice on one batch. A person CAN be both a guide and coordinator on different batches.
💡
Enforce exactly one trek_leader per active batch at application layer (check before INSERT/UPDATE, not DB constraint, to allow graceful leader handover).

2.3 Enum: batch_staff_role

Role value Who holds it Key permissions
trek_leader 1 per batch (required) Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin
guide 1–N per batch (min 1 required) Read-only access to participant list and docs · View day itinerary · Cannot change status
coordinator 0–N per batch (optional) Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member

2.4 New table: batch_status_history

Append-only audit trail of every status transition. Never update or delete rows.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE (audit trail is batch-scoped)
from_status trek_batch_status null if first transition (scheduled → ...)
to_status trek_batch_status The new status
changed_by uuid FK → users Must be the trek_leader for operational transitions
changed_at timestamptz Server time, not client time
note text Optional leader note at time of transition
location_lat decimal(9,6) Optional GPS at time of status change
location_lng decimal(9,6) Optional GPS at time of status change

2.5 New table: loyalty_pending_credits

Staging table used by the post-completion loyalty job to ensure idempotency.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches
user_id uuid FK → users Trekker who completed the batch
points_to_credit integer Computed at job time from trek difficulty + loyalty rules
status credit_status (enum) pending | processing | credited | failed
job_run_id uuid BullMQ job ID — idempotency key
credited_at timestamptz Set when loyalty_points row is inserted
error_detail text Populated on failure for retry/manual review
💡
Unique constraint on (batch_id, user_id) — guarantees one credit row per trekker per batch regardless of retries.

3. Trek Batch Status Lifecycle

3.1 Status enum values

Status Meaning Who can set it Trigger
scheduled Batch is open for bookings; no trek activity yet System (default on create) Admin creates batch
assembling Bookings closed; participants are gathering at meeting point Trek Leader or Admin Leader taps 'Start assembly' — typically morning of Day 1
in_progress Trek is actively underway Trek Leader only Leader taps 'Begin trek' after headcount at assembly
paused Trek temporarily halted (weather window, medical situation, route issue) Trek Leader only Leader taps 'Pause trek'; must add a note explaining reason
completed Trek finished; all participants safely returned Trek Leader only Leader taps 'Complete trek'; triggers loyalty job
cancelled Batch will not run Admin only (not leader) Admin action; triggers refund workflow

3.2 Allowed transitions

From → → To Allowed? Notes
scheduled assembling Yes Leader or admin; closes new bookings
scheduled cancelled Yes Admin only
assembling in_progress Yes Leader only; sets started_at
assembling cancelled Yes Admin only (e.g. sudden weather)
in_progress paused Yes Leader only; note required
in_progress completed Yes Leader only; sets finished_at, triggers loyalty job
in_progress cancelled No Cannot cancel a running trek — use completed with incident note
paused in_progress Yes Leader only; resumes trek
paused completed Yes Leader only; if trek ends while paused (e.g. emergency extraction)
completed any No Terminal state — no further transitions
cancelled any No Terminal state — no further transitions

3.3 Status transition API

PATCH /api/v1/batches/:id/status

Request body: { to_status, note?, location? }

Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.

Server-side logic on this endpoint:

  • Validate transition is in the allowed matrix above; return 422 if not
  • Check caller is active trek_leader on this batch (or admin)
  • Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
  • INSERT into batch_status_history
  • If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
  • If → in_progress or assembling: send push notification to all confirmed participants
  • Return updated batch object

4. API Routes — New & Updated

4.1 Batch staff management (Admin)

Method Path Description
GET /api/v1/admin/batches/:id/staff List all staff assigned to a batch with roles
POST /api/v1/admin/batches/:id/staff Assign a user to a batch with a role. Body: { user_id, role, notes? }
PATCH /api/v1/admin/batches/:id/staff/:staffId Update role or notes for a staff member
DELETE /api/v1/admin/batches/:id/staff/:staffId Remove (set is_active=false) a staff member from a batch
GET /api/v1/admin/users?role=trek_leader List users eligible to be assigned as leaders

4.2 Batch status management (Trek Leader / Admin)

Method Path Description
PATCH /api/v1/batches/:id/status Transition batch status. Body: { to_status, note?, location_lat?, location_lng? }
GET /api/v1/batches/:id/status-history Full audit log of all status transitions for a batch
PATCH /api/v1/batches/:id/incident Trek leader posts/updates incident note on a running batch
GET /api/v1/batches/live Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed)
GET /api/v1/batches/:id/live-status Real-time status card for a single batch (polling or SSE)

4.3 Leader-facing mobile routes

These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.

Method Path Description
GET /api/v1/leader/batches Leader's assigned batches: upcoming, live, past
GET /api/v1/leader/batches/:id Full batch detail including participant list, docs status, staff roster
GET /api/v1/leader/batches/:id/participants All confirmed participants with doc/health status and emergency contacts
POST /api/v1/leader/batches/:id/notify Send push notification to all batch participants. Body: { title, message }

4.4 Guide-facing mobile routes

Method Path Description
GET /api/v1/guide/batches Guide's assigned batches
GET /api/v1/guide/batches/:id Batch detail — itinerary, participant count, emergency contacts (no payment data)
GET /api/v1/guide/batches/:id/participants Participant names and emergency contacts only

5. Frontend — Ongoing Treks Feature

5.1 Website: Ongoing Treks section

A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.

Element Data source Behaviour
'Live now' badge batch.status = in_progress Pulsing green dot; visible on trek card in explore listing
'Assembling' badge batch.status = assembling Amber badge — 'Starting soon'
Ongoing Treks section (home) GET /api/v1/batches/live Horizontal scroll card strip; shows trek name, region, day N of M, leader name
Live status card GET /api/v1/batches/:id/live-status (poll 60s) Current status, day number, last status update time, incident note if any
/treks/ongoing page Same endpoint Full grid of all ongoing batches; filterable by region

5.2 Mobile app: Trek Leader screens

A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.

Screen Route Description
Leader home app/(tabs)/leader/index.tsx Cards for each assigned batch: upcoming / live / past
Batch control panel app/(tabs)/leader/batch/[id].tsx Status pill, big action button (context-aware), participant count, staff list
Status transition app/(tabs)/leader/batch/[id]/status.tsx Confirmation sheet with allowed next states, mandatory note input for pause/complete
Participant list app/(tabs)/leader/batch/[id]/participants.tsx Trekker names, doc status indicators, emergency contact tap-to-call
Notify participants app/(tabs)/leader/batch/[id]/notify.tsx Title + message compose; sends push to all confirmed participants
Status history app/(tabs)/leader/batch/[id]/history.tsx Timeline of all status changes with timestamps and notes
Incident log app/(tabs)/leader/batch/[id]/incident.tsx Free-text incident note; visible to admin, not to participants

5.3 Mobile app: Guide screens

Screen Route Description
Guide home app/(tabs)/guide/index.tsx Upcoming and active batch assignments
Batch view app/(tabs)/guide/batch/[id].tsx Read-only: itinerary, participant count, day-wise plan
Participant contacts app/(tabs)/guide/batch/[id]/contacts.tsx Names + emergency contacts (tap-to-call); no booking/payment data

5.4 Participant-facing: ongoing trek card

In the My Treks tab, a booking with an active batch shows an enhanced card:

  • Live status banner: 'Your trek is in progress — Day 3 of 7'
  • Last update timestamp from status_updated_at
  • Guide contact buttons (tap-to-call/WhatsApp for each guide)
  • Trek leader name and emergency contact
  • Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')

6. Loyalty Credit on Trek Completion

6.1 Credit rules

Trek difficulty Base points awarded Notes
Easy 100 pts e.g. Kedarkantha meadows, beginner treks
Moderate 200 pts Most standard Himalayan treks
Difficult 350 pts Technical routes, high-altitude passes
Expedition 500 pts Summit attempts, multi-week treks

Multipliers applied on top of base points:

Multiplier condition Multiplier Example
First completed trek (new user) 1.5× Easy first trek: 100 × 1.5 = 150 pts
Repeat booking (2nd+ trek with OBS) 1.1× Moderate repeat: 200 × 1.1 = 220 pts
Trek completed in current season peak (configurable) 1.2× Peak season bonus
Full group (batch ≥ 90% full) 1.1× Rewarding popular batches
Multipliers stack multiplicatively Repeat + peak: 200 × 1.1 × 1.2 = 264 pts

6.2 BullMQ job: credit_loyalty_on_completion

Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.

Step Action Error handling
1 — Fetch participants Query all bookings WHERE batch_id = X AND status = 'confirmed' If no confirmed bookings: log and exit cleanly
2 — Check idempotency For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin.
3 — Insert pending rows INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee
4 — Compute points For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers Round to nearest integer
5 — Credit points INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry.
6 — Update tier Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed Run per-user after credit. Non-blocking — tier is cosmetic.
7 — Notify Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' Notification failure does not roll back credits
8 — Award certificate Enqueue separate generate_certificate job per user Separate job — idempotent; certificate URL stored on booking

6.3 Points→tier recomputation

Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).

7. Admin Panel Additions

7.1 Batch staff assignment UI

  • On any batch detail page in admin, a 'Staff' tab shows current assignments
  • 'Assign staff' button opens a user search modal; select user → pick role → save
  • Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
  • 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note

7.2 Live batch monitor

New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):

  • Batch name, trek, day N of M, leader name, participant count, last status update
  • Quick-expand to see incident notes and full status history
  • 'Contact leader' button (WhatsApp deep link or in-app message)
  • Admin can force-complete or cancel any running batch from here (with mandatory reason)

7.3 Loyalty audit dashboard

  • Table of all loyalty_pending_credits rows filterable by status
  • 'Failed' rows shown prominently with error_detail and a manual retry button
  • Batch completion summary: which batches have been credited, how many users, total points awarded

8. Migration Plan (Existing Data)

Step Action Notes
1 Run migration: add new columns to trek_batches (status, status_updated_at etc) All existing batches get status = 'scheduled' by default
2 Create new tables: batch_staff, batch_status_history, loyalty_pending_credits No data migration needed — tables start empty
3 Create enum types: trek_batch_status, batch_staff_role, credit_status Do this before altering tables that reference them
4 Migrate existing guide_id FK data For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id)
5 Drop guide_id column from trek_batches After verifying migration in step 4 is complete
6 Backfill status_history Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness
💡
Run all migration steps inside a single database transaction. Use a Prisma migration file with raw SQL for the enum creation (Prisma does not auto-generate enum creation in all cases).

OBS EXPERIENCES

Addendum: Trek Batch Roles, Live Status & Loyalty

Extends MVP Architecture v1.0 | June 2026

1. Overview & Design Principles

Extends MVP Architecture v1.0 | Added June 2026

This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.

Three principles guide the design:

  • Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
  • Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
  • Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.

2. Database Schema Changes

2.1 Updated trek_batches table

Existing columns carry forward. New and changed columns are marked.

Column Type Default Notes
id uuid PK gen_random_uuid() No change
trek_id uuid FK → treks No change
start_date date No change
end_date date No change
price_inr integer No change
seats_total integer No change
seats_booked integer 0 No change
seats_held integer 0 No change
status trek_batch_status (enum) 'scheduled' NEW — see lifecycle §3
status_updated_at timestamptz now() NEW — when status last changed
status_updated_by uuid FK → users null NEW — leader who changed it
started_at timestamptz null NEW — set when status → in_progress
finished_at timestamptz null NEW — set when status → completed
actual_end_date date null NEW — may differ from planned end_date
incident_notes text null NEW — free text, leader-only write
cancellation_reason text null No change
meeting_point text null NEW — e.g. 'Sankri base camp parking'
meeting_time timestamptz null NEW — assembly time day 1
💡
Add a Postgres enum: CREATE TYPE trek_batch_status AS ENUM ('scheduled','assembling','in_progress','paused','completed','cancelled');

2.2 New table: batch_staff

Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE
user_id uuid FK → users Staff must have a user account
role batch_staff_role (enum) trek_leader | guide | coordinator — see §2.3
assigned_by uuid FK → users Admin who made the assignment
assigned_at timestamptz Timestamp of assignment
is_active boolean true = currently on this batch; false = removed/replaced
notes text Internal notes (e.g. 'backup leader', 'local area expert')
💡
Unique constraint: (batch_id, user_id, role) — same person cannot hold the same role twice on one batch. A person CAN be both a guide and coordinator on different batches.
💡
Enforce exactly one trek_leader per active batch at application layer (check before INSERT/UPDATE, not DB constraint, to allow graceful leader handover).

2.3 Enum: batch_staff_role

Role value Who holds it Key permissions
trek_leader 1 per batch (required) Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin
guide 1–N per batch (min 1 required) Read-only access to participant list and docs · View day itinerary · Cannot change status
coordinator 0–N per batch (optional) Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member

2.4 New table: batch_status_history

Append-only audit trail of every status transition. Never update or delete rows.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE (audit trail is batch-scoped)
from_status trek_batch_status null if first transition (scheduled → ...)
to_status trek_batch_status The new status
changed_by uuid FK → users Must be the trek_leader for operational transitions
changed_at timestamptz Server time, not client time
note text Optional leader note at time of transition
location_lat decimal(9,6) Optional GPS at time of status change
location_lng decimal(9,6) Optional GPS at time of status change

2.5 New table: loyalty_pending_credits

Staging table used by the post-completion loyalty job to ensure idempotency.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches
user_id uuid FK → users Trekker who completed the batch
points_to_credit integer Computed at job time from trek difficulty + loyalty rules
status credit_status (enum) pending | processing | credited | failed
job_run_id uuid BullMQ job ID — idempotency key
credited_at timestamptz Set when loyalty_points row is inserted
error_detail text Populated on failure for retry/manual review
💡
Unique constraint on (batch_id, user_id) — guarantees one credit row per trekker per batch regardless of retries.

3. Trek Batch Status Lifecycle

3.1 Status enum values

Status Meaning Who can set it Trigger
scheduled Batch is open for bookings; no trek activity yet System (default on create) Admin creates batch
assembling Bookings closed; participants are gathering at meeting point Trek Leader or Admin Leader taps 'Start assembly' — typically morning of Day 1
in_progress Trek is actively underway Trek Leader only Leader taps 'Begin trek' after headcount at assembly
paused Trek temporarily halted (weather window, medical situation, route issue) Trek Leader only Leader taps 'Pause trek'; must add a note explaining reason
completed Trek finished; all participants safely returned Trek Leader only Leader taps 'Complete trek'; triggers loyalty job
cancelled Batch will not run Admin only (not leader) Admin action; triggers refund workflow

3.2 Allowed transitions

From → → To Allowed? Notes
scheduled assembling Yes Leader or admin; closes new bookings
scheduled cancelled Yes Admin only
assembling in_progress Yes Leader only; sets started_at
assembling cancelled Yes Admin only (e.g. sudden weather)
in_progress paused Yes Leader only; note required
in_progress completed Yes Leader only; sets finished_at, triggers loyalty job
in_progress cancelled No Cannot cancel a running trek — use completed with incident note
paused in_progress Yes Leader only; resumes trek
paused completed Yes Leader only; if trek ends while paused (e.g. emergency extraction)
completed any No Terminal state — no further transitions
cancelled any No Terminal state — no further transitions

3.3 Status transition API

PATCH /api/v1/batches/:id/status

Request body: { to_status, note?, location? }

Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.

Server-side logic on this endpoint:

  • Validate transition is in the allowed matrix above; return 422 if not
  • Check caller is active trek_leader on this batch (or admin)
  • Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
  • INSERT into batch_status_history
  • If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
  • If → in_progress or assembling: send push notification to all confirmed participants
  • Return updated batch object

4. API Routes — New & Updated

4.1 Batch staff management (Admin)

Method Path Description
GET /api/v1/admin/batches/:id/staff List all staff assigned to a batch with roles
POST /api/v1/admin/batches/:id/staff Assign a user to a batch with a role. Body: { user_id, role, notes? }
PATCH /api/v1/admin/batches/:id/staff/:staffId Update role or notes for a staff member
DELETE /api/v1/admin/batches/:id/staff/:staffId Remove (set is_active=false) a staff member from a batch
GET /api/v1/admin/users?role=trek_leader List users eligible to be assigned as leaders

4.2 Batch status management (Trek Leader / Admin)

Method Path Description
PATCH /api/v1/batches/:id/status Transition batch status. Body: { to_status, note?, location_lat?, location_lng? }
GET /api/v1/batches/:id/status-history Full audit log of all status transitions for a batch
PATCH /api/v1/batches/:id/incident Trek leader posts/updates incident note on a running batch
GET /api/v1/batches/live Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed)
GET /api/v1/batches/:id/live-status Real-time status card for a single batch (polling or SSE)

4.3 Leader-facing mobile routes

These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.

Method Path Description
GET /api/v1/leader/batches Leader's assigned batches: upcoming, live, past
GET /api/v1/leader/batches/:id Full batch detail including participant list, docs status, staff roster
GET /api/v1/leader/batches/:id/participants All confirmed participants with doc/health status and emergency contacts
POST /api/v1/leader/batches/:id/notify Send push notification to all batch participants. Body: { title, message }

4.4 Guide-facing mobile routes

Method Path Description
GET /api/v1/guide/batches Guide's assigned batches
GET /api/v1/guide/batches/:id Batch detail — itinerary, participant count, emergency contacts (no payment data)
GET /api/v1/guide/batches/:id/participants Participant names and emergency contacts only

5. Frontend — Ongoing Treks Feature

5.1 Website: Ongoing Treks section

A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.

Element Data source Behaviour
'Live now' badge batch.status = in_progress Pulsing green dot; visible on trek card in explore listing
'Assembling' badge batch.status = assembling Amber badge — 'Starting soon'
Ongoing Treks section (home) GET /api/v1/batches/live Horizontal scroll card strip; shows trek name, region, day N of M, leader name
Live status card GET /api/v1/batches/:id/live-status (poll 60s) Current status, day number, last status update time, incident note if any
/treks/ongoing page Same endpoint Full grid of all ongoing batches; filterable by region

5.2 Mobile app: Trek Leader screens

A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.

Screen Route Description
Leader home app/(tabs)/leader/index.tsx Cards for each assigned batch: upcoming / live / past
Batch control panel app/(tabs)/leader/batch/[id].tsx Status pill, big action button (context-aware), participant count, staff list
Status transition app/(tabs)/leader/batch/[id]/status.tsx Confirmation sheet with allowed next states, mandatory note input for pause/complete
Participant list app/(tabs)/leader/batch/[id]/participants.tsx Trekker names, doc status indicators, emergency contact tap-to-call
Notify participants app/(tabs)/leader/batch/[id]/notify.tsx Title + message compose; sends push to all confirmed participants
Status history app/(tabs)/leader/batch/[id]/history.tsx Timeline of all status changes with timestamps and notes
Incident log app/(tabs)/leader/batch/[id]/incident.tsx Free-text incident note; visible to admin, not to participants

5.3 Mobile app: Guide screens

Screen Route Description
Guide home app/(tabs)/guide/index.tsx Upcoming and active batch assignments
Batch view app/(tabs)/guide/batch/[id].tsx Read-only: itinerary, participant count, day-wise plan
Participant contacts app/(tabs)/guide/batch/[id]/contacts.tsx Names + emergency contacts (tap-to-call); no booking/payment data

5.4 Participant-facing: ongoing trek card

In the My Treks tab, a booking with an active batch shows an enhanced card:

  • Live status banner: 'Your trek is in progress — Day 3 of 7'
  • Last update timestamp from status_updated_at
  • Guide contact buttons (tap-to-call/WhatsApp for each guide)
  • Trek leader name and emergency contact
  • Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')

6. Loyalty Credit on Trek Completion

6.1 Credit rules

Trek difficulty Base points awarded Notes
Easy 100 pts e.g. Kedarkantha meadows, beginner treks
Moderate 200 pts Most standard Himalayan treks
Difficult 350 pts Technical routes, high-altitude passes
Expedition 500 pts Summit attempts, multi-week treks

Multipliers applied on top of base points:

Multiplier condition Multiplier Example
First completed trek (new user) 1.5× Easy first trek: 100 × 1.5 = 150 pts
Repeat booking (2nd+ trek with OBS) 1.1× Moderate repeat: 200 × 1.1 = 220 pts
Trek completed in current season peak (configurable) 1.2× Peak season bonus
Full group (batch ≥ 90% full) 1.1× Rewarding popular batches
Multipliers stack multiplicatively Repeat + peak: 200 × 1.1 × 1.2 = 264 pts

6.2 BullMQ job: credit_loyalty_on_completion

Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.

Step Action Error handling
1 — Fetch participants Query all bookings WHERE batch_id = X AND status = 'confirmed' If no confirmed bookings: log and exit cleanly
2 — Check idempotency For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin.
3 — Insert pending rows INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee
4 — Compute points For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers Round to nearest integer
5 — Credit points INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry.
6 — Update tier Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed Run per-user after credit. Non-blocking — tier is cosmetic.
7 — Notify Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' Notification failure does not roll back credits
8 — Award certificate Enqueue separate generate_certificate job per user Separate job — idempotent; certificate URL stored on booking

6.3 Points→tier recomputation

Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).

7. Admin Panel Additions

7.1 Batch staff assignment UI

  • On any batch detail page in admin, a 'Staff' tab shows current assignments
  • 'Assign staff' button opens a user search modal; select user → pick role → save
  • Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
  • 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note

7.2 Live batch monitor

New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):

  • Batch name, trek, day N of M, leader name, participant count, last status update
  • Quick-expand to see incident notes and full status history
  • 'Contact leader' button (WhatsApp deep link or in-app message)
  • Admin can force-complete or cancel any running batch from here (with mandatory reason)

7.3 Loyalty audit dashboard

  • Table of all loyalty_pending_credits rows filterable by status
  • 'Failed' rows shown prominently with error_detail and a manual retry button
  • Batch completion summary: which batches have been credited, how many users, total points awarded

8. Migration Plan (Existing Data)

Step Action Notes
1 Run migration: add new columns to trek_batches (status, status_updated_at etc) All existing batches get status = 'scheduled' by default
2 Create new tables: batch_staff, batch_status_history, loyalty_pending_credits No data migration needed — tables start empty
3 Create enum types: trek_batch_status, batch_staff_role, credit_status Do this before altering tables that reference them
4 Migrate existing guide_id FK data For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id)
5 Drop guide_id column from trek_batches After verifying migration in step 4 is complete
6 Backfill status_history Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness
💡
Run all migration steps inside a single database transaction. Use a Prisma migration file with raw SQL for the enum creation (Prisma does not auto-generate enum creation in all cases).

OBS EXPERIENCES

Addendum: Trek Batch Roles, Live Status & Loyalty

Extends MVP Architecture v1.0 | June 2026

1. Overview & Design Principles

Extends MVP Architecture v1.0 | Added June 2026

This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.

Three principles guide the design:

  • Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
  • Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
  • Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.

2. Database Schema Changes

2.1 Updated trek_batches table

Existing columns carry forward. New and changed columns are marked.

Column Type Default Notes
id uuid PK gen_random_uuid() No change
trek_id uuid FK → treks No change
start_date date No change
end_date date No change
price_inr integer No change
seats_total integer No change
seats_booked integer 0 No change
seats_held integer 0 No change
status trek_batch_status (enum) 'scheduled' NEW — see lifecycle §3
status_updated_at timestamptz now() NEW — when status last changed
status_updated_by uuid FK → users null NEW — leader who changed it
started_at timestamptz null NEW — set when status → in_progress
finished_at timestamptz null NEW — set when status → completed
actual_end_date date null NEW — may differ from planned end_date
incident_notes text null NEW — free text, leader-only write
cancellation_reason text null No change
meeting_point text null NEW — e.g. 'Sankri base camp parking'
meeting_time timestamptz null NEW — assembly time day 1
💡
Add a Postgres enum: CREATE TYPE trek_batch_status AS ENUM ('scheduled','assembling','in_progress','paused','completed','cancelled');

2.2 New table: batch_staff

Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE
user_id uuid FK → users Staff must have a user account
role batch_staff_role (enum) trek_leader | guide | coordinator — see §2.3
assigned_by uuid FK → users Admin who made the assignment
assigned_at timestamptz Timestamp of assignment
is_active boolean true = currently on this batch; false = removed/replaced
notes text Internal notes (e.g. 'backup leader', 'local area expert')
💡
Unique constraint: (batch_id, user_id, role) — same person cannot hold the same role twice on one batch. A person CAN be both a guide and coordinator on different batches.
💡
Enforce exactly one trek_leader per active batch at application layer (check before INSERT/UPDATE, not DB constraint, to allow graceful leader handover).

2.3 Enum: batch_staff_role

Role value Who holds it Key permissions
trek_leader 1 per batch (required) Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin
guide 1–N per batch (min 1 required) Read-only access to participant list and docs · View day itinerary · Cannot change status
coordinator 0–N per batch (optional) Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member

2.4 New table: batch_status_history

Append-only audit trail of every status transition. Never update or delete rows.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE (audit trail is batch-scoped)
from_status trek_batch_status null if first transition (scheduled → ...)
to_status trek_batch_status The new status
changed_by uuid FK → users Must be the trek_leader for operational transitions
changed_at timestamptz Server time, not client time
note text Optional leader note at time of transition
location_lat decimal(9,6) Optional GPS at time of status change
location_lng decimal(9,6) Optional GPS at time of status change

2.5 New table: loyalty_pending_credits

Staging table used by the post-completion loyalty job to ensure idempotency.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches
user_id uuid FK → users Trekker who completed the batch
points_to_credit integer Computed at job time from trek difficulty + loyalty rules
status credit_status (enum) pending | processing | credited | failed
job_run_id uuid BullMQ job ID — idempotency key
credited_at timestamptz Set when loyalty_points row is inserted
error_detail text Populated on failure for retry/manual review
💡
Unique constraint on (batch_id, user_id) — guarantees one credit row per trekker per batch regardless of retries.

3. Trek Batch Status Lifecycle

3.1 Status enum values

Status Meaning Who can set it Trigger
scheduled Batch is open for bookings; no trek activity yet System (default on create) Admin creates batch
assembling Bookings closed; participants are gathering at meeting point Trek Leader or Admin Leader taps 'Start assembly' — typically morning of Day 1
in_progress Trek is actively underway Trek Leader only Leader taps 'Begin trek' after headcount at assembly
paused Trek temporarily halted (weather window, medical situation, route issue) Trek Leader only Leader taps 'Pause trek'; must add a note explaining reason
completed Trek finished; all participants safely returned Trek Leader only Leader taps 'Complete trek'; triggers loyalty job
cancelled Batch will not run Admin only (not leader) Admin action; triggers refund workflow

3.2 Allowed transitions

From → → To Allowed? Notes
scheduled assembling Yes Leader or admin; closes new bookings
scheduled cancelled Yes Admin only
assembling in_progress Yes Leader only; sets started_at
assembling cancelled Yes Admin only (e.g. sudden weather)
in_progress paused Yes Leader only; note required
in_progress completed Yes Leader only; sets finished_at, triggers loyalty job
in_progress cancelled No Cannot cancel a running trek — use completed with incident note
paused in_progress Yes Leader only; resumes trek
paused completed Yes Leader only; if trek ends while paused (e.g. emergency extraction)
completed any No Terminal state — no further transitions
cancelled any No Terminal state — no further transitions

3.3 Status transition API

PATCH /api/v1/batches/:id/status

Request body: { to_status, note?, location? }

Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.

Server-side logic on this endpoint:

  • Validate transition is in the allowed matrix above; return 422 if not
  • Check caller is active trek_leader on this batch (or admin)
  • Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
  • INSERT into batch_status_history
  • If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
  • If → in_progress or assembling: send push notification to all confirmed participants
  • Return updated batch object

4. API Routes — New & Updated

4.1 Batch staff management (Admin)

Method Path Description
GET /api/v1/admin/batches/:id/staff List all staff assigned to a batch with roles
POST /api/v1/admin/batches/:id/staff Assign a user to a batch with a role. Body: { user_id, role, notes? }
PATCH /api/v1/admin/batches/:id/staff/:staffId Update role or notes for a staff member
DELETE /api/v1/admin/batches/:id/staff/:staffId Remove (set is_active=false) a staff member from a batch
GET /api/v1/admin/users?role=trek_leader List users eligible to be assigned as leaders

4.2 Batch status management (Trek Leader / Admin)

Method Path Description
PATCH /api/v1/batches/:id/status Transition batch status. Body: { to_status, note?, location_lat?, location_lng? }
GET /api/v1/batches/:id/status-history Full audit log of all status transitions for a batch
PATCH /api/v1/batches/:id/incident Trek leader posts/updates incident note on a running batch
GET /api/v1/batches/live Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed)
GET /api/v1/batches/:id/live-status Real-time status card for a single batch (polling or SSE)

4.3 Leader-facing mobile routes

These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.

Method Path Description
GET /api/v1/leader/batches Leader's assigned batches: upcoming, live, past
GET /api/v1/leader/batches/:id Full batch detail including participant list, docs status, staff roster
GET /api/v1/leader/batches/:id/participants All confirmed participants with doc/health status and emergency contacts
POST /api/v1/leader/batches/:id/notify Send push notification to all batch participants. Body: { title, message }

4.4 Guide-facing mobile routes

Method Path Description
GET /api/v1/guide/batches Guide's assigned batches
GET /api/v1/guide/batches/:id Batch detail — itinerary, participant count, emergency contacts (no payment data)
GET /api/v1/guide/batches/:id/participants Participant names and emergency contacts only

5. Frontend — Ongoing Treks Feature

5.1 Website: Ongoing Treks section

A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.

Element Data source Behaviour
'Live now' badge batch.status = in_progress Pulsing green dot; visible on trek card in explore listing
'Assembling' badge batch.status = assembling Amber badge — 'Starting soon'
Ongoing Treks section (home) GET /api/v1/batches/live Horizontal scroll card strip; shows trek name, region, day N of M, leader name
Live status card GET /api/v1/batches/:id/live-status (poll 60s) Current status, day number, last status update time, incident note if any
/treks/ongoing page Same endpoint Full grid of all ongoing batches; filterable by region

5.2 Mobile app: Trek Leader screens

A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.

Screen Route Description
Leader home app/(tabs)/leader/index.tsx Cards for each assigned batch: upcoming / live / past
Batch control panel app/(tabs)/leader/batch/[id].tsx Status pill, big action button (context-aware), participant count, staff list
Status transition app/(tabs)/leader/batch/[id]/status.tsx Confirmation sheet with allowed next states, mandatory note input for pause/complete
Participant list app/(tabs)/leader/batch/[id]/participants.tsx Trekker names, doc status indicators, emergency contact tap-to-call
Notify participants app/(tabs)/leader/batch/[id]/notify.tsx Title + message compose; sends push to all confirmed participants
Status history app/(tabs)/leader/batch/[id]/history.tsx Timeline of all status changes with timestamps and notes
Incident log app/(tabs)/leader/batch/[id]/incident.tsx Free-text incident note; visible to admin, not to participants

5.3 Mobile app: Guide screens

Screen Route Description
Guide home app/(tabs)/guide/index.tsx Upcoming and active batch assignments
Batch view app/(tabs)/guide/batch/[id].tsx Read-only: itinerary, participant count, day-wise plan
Participant contacts app/(tabs)/guide/batch/[id]/contacts.tsx Names + emergency contacts (tap-to-call); no booking/payment data

5.4 Participant-facing: ongoing trek card

In the My Treks tab, a booking with an active batch shows an enhanced card:

  • Live status banner: 'Your trek is in progress — Day 3 of 7'
  • Last update timestamp from status_updated_at
  • Guide contact buttons (tap-to-call/WhatsApp for each guide)
  • Trek leader name and emergency contact
  • Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')

6. Loyalty Credit on Trek Completion

6.1 Credit rules

Trek difficulty Base points awarded Notes
Easy 100 pts e.g. Kedarkantha meadows, beginner treks
Moderate 200 pts Most standard Himalayan treks
Difficult 350 pts Technical routes, high-altitude passes
Expedition 500 pts Summit attempts, multi-week treks

Multipliers applied on top of base points:

Multiplier condition Multiplier Example
First completed trek (new user) 1.5× Easy first trek: 100 × 1.5 = 150 pts
Repeat booking (2nd+ trek with OBS) 1.1× Moderate repeat: 200 × 1.1 = 220 pts
Trek completed in current season peak (configurable) 1.2× Peak season bonus
Full group (batch ≥ 90% full) 1.1× Rewarding popular batches
Multipliers stack multiplicatively Repeat + peak: 200 × 1.1 × 1.2 = 264 pts

6.2 BullMQ job: credit_loyalty_on_completion

Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.

Step Action Error handling
1 — Fetch participants Query all bookings WHERE batch_id = X AND status = 'confirmed' If no confirmed bookings: log and exit cleanly
2 — Check idempotency For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin.
3 — Insert pending rows INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee
4 — Compute points For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers Round to nearest integer
5 — Credit points INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry.
6 — Update tier Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed Run per-user after credit. Non-blocking — tier is cosmetic.
7 — Notify Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' Notification failure does not roll back credits
8 — Award certificate Enqueue separate generate_certificate job per user Separate job — idempotent; certificate URL stored on booking

6.3 Points→tier recomputation

Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).

7. Admin Panel Additions

7.1 Batch staff assignment UI

  • On any batch detail page in admin, a 'Staff' tab shows current assignments
  • 'Assign staff' button opens a user search modal; select user → pick role → save
  • Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
  • 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note

7.2 Live batch monitor

New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):

  • Batch name, trek, day N of M, leader name, participant count, last status update
  • Quick-expand to see incident notes and full status history
  • 'Contact leader' button (WhatsApp deep link or in-app message)
  • Admin can force-complete or cancel any running batch from here (with mandatory reason)

7.3 Loyalty audit dashboard

  • Table of all loyalty_pending_credits rows filterable by status
  • 'Failed' rows shown prominently with error_detail and a manual retry button
  • Batch completion summary: which batches have been credited, how many users, total points awarded

8. Migration Plan (Existing Data)

Step Action Notes
1 Run migration: add new columns to trek_batches (status, status_updated_at etc) All existing batches get status = 'scheduled' by default
2 Create new tables: batch_staff, batch_status_history, loyalty_pending_credits No data migration needed — tables start empty
3 Create enum types: trek_batch_status, batch_staff_role, credit_status Do this before altering tables that reference them
4 Migrate existing guide_id FK data For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id)
5 Drop guide_id column from trek_batches After verifying migration in step 4 is complete
6 Backfill status_history Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness
💡
Run all migration steps inside a single database transaction. Use a Prisma migration file with raw SQL for the enum creation (Prisma does not auto-generate enum creation in all cases).

OBS EXPERIENCES

Addendum: Trek Batch Roles, Live Status & Loyalty

Extends MVP Architecture v1.0 | June 2026

1. Overview & Design Principles

Extends MVP Architecture v1.0 | Added June 2026

This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.

Three principles guide the design:

  • Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
  • Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
  • Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.

2. Database Schema Changes

2.1 Updated trek_batches table

Existing columns carry forward. New and changed columns are marked.

Column Type Default Notes
id uuid PK gen_random_uuid() No change
trek_id uuid FK → treks No change
start_date date No change
end_date date No change
price_inr integer No change
seats_total integer No change
seats_booked integer 0 No change
seats_held integer 0 No change
status trek_batch_status (enum) 'scheduled' NEW — see lifecycle §3
status_updated_at timestamptz now() NEW — when status last changed
status_updated_by uuid FK → users null NEW — leader who changed it
started_at timestamptz null NEW — set when status → in_progress
finished_at timestamptz null NEW — set when status → completed
actual_end_date date null NEW — may differ from planned end_date
incident_notes text null NEW — free text, leader-only write
cancellation_reason text null No change
meeting_point text null NEW — e.g. 'Sankri base camp parking'
meeting_time timestamptz null NEW — assembly time day 1
💡
Add a Postgres enum: CREATE TYPE trek_batch_status AS ENUM ('scheduled','assembling','in_progress','paused','completed','cancelled');

2.2 New table: batch_staff

Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE
user_id uuid FK → users Staff must have a user account
role batch_staff_role (enum) trek_leader | guide | coordinator — see §2.3
assigned_by uuid FK → users Admin who made the assignment
assigned_at timestamptz Timestamp of assignment
is_active boolean true = currently on this batch; false = removed/replaced
notes text Internal notes (e.g. 'backup leader', 'local area expert')
💡
Unique constraint: (batch_id, user_id, role) — same person cannot hold the same role twice on one batch. A person CAN be both a guide and coordinator on different batches.
💡
Enforce exactly one trek_leader per active batch at application layer (check before INSERT/UPDATE, not DB constraint, to allow graceful leader handover).

2.3 Enum: batch_staff_role

Role value Who holds it Key permissions
trek_leader 1 per batch (required) Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin
guide 1–N per batch (min 1 required) Read-only access to participant list and docs · View day itinerary · Cannot change status
coordinator 0–N per batch (optional) Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member

2.4 New table: batch_status_history

Append-only audit trail of every status transition. Never update or delete rows.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE (audit trail is batch-scoped)
from_status trek_batch_status null if first transition (scheduled → ...)
to_status trek_batch_status The new status
changed_by uuid FK → users Must be the trek_leader for operational transitions
changed_at timestamptz Server time, not client time
note text Optional leader note at time of transition
location_lat decimal(9,6) Optional GPS at time of status change
location_lng decimal(9,6) Optional GPS at time of status change

2.5 New table: loyalty_pending_credits

Staging table used by the post-completion loyalty job to ensure idempotency.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches
user_id uuid FK → users Trekker who completed the batch
points_to_credit integer Computed at job time from trek difficulty + loyalty rules
status credit_status (enum) pending | processing | credited | failed
job_run_id uuid BullMQ job ID — idempotency key
credited_at timestamptz Set when loyalty_points row is inserted
error_detail text Populated on failure for retry/manual review
💡
Unique constraint on (batch_id, user_id) — guarantees one credit row per trekker per batch regardless of retries.

3. Trek Batch Status Lifecycle

3.1 Status enum values

Status Meaning Who can set it Trigger
scheduled Batch is open for bookings; no trek activity yet System (default on create) Admin creates batch
assembling Bookings closed; participants are gathering at meeting point Trek Leader or Admin Leader taps 'Start assembly' — typically morning of Day 1
in_progress Trek is actively underway Trek Leader only Leader taps 'Begin trek' after headcount at assembly
paused Trek temporarily halted (weather window, medical situation, route issue) Trek Leader only Leader taps 'Pause trek'; must add a note explaining reason
completed Trek finished; all participants safely returned Trek Leader only Leader taps 'Complete trek'; triggers loyalty job
cancelled Batch will not run Admin only (not leader) Admin action; triggers refund workflow

3.2 Allowed transitions

From → → To Allowed? Notes
scheduled assembling Yes Leader or admin; closes new bookings
scheduled cancelled Yes Admin only
assembling in_progress Yes Leader only; sets started_at
assembling cancelled Yes Admin only (e.g. sudden weather)
in_progress paused Yes Leader only; note required
in_progress completed Yes Leader only; sets finished_at, triggers loyalty job
in_progress cancelled No Cannot cancel a running trek — use completed with incident note
paused in_progress Yes Leader only; resumes trek
paused completed Yes Leader only; if trek ends while paused (e.g. emergency extraction)
completed any No Terminal state — no further transitions
cancelled any No Terminal state — no further transitions

3.3 Status transition API

PATCH /api/v1/batches/:id/status

Request body: { to_status, note?, location? }

Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.

Server-side logic on this endpoint:

  • Validate transition is in the allowed matrix above; return 422 if not
  • Check caller is active trek_leader on this batch (or admin)
  • Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
  • INSERT into batch_status_history
  • If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
  • If → in_progress or assembling: send push notification to all confirmed participants
  • Return updated batch object

4. API Routes — New & Updated

4.1 Batch staff management (Admin)

Method Path Description
GET /api/v1/admin/batches/:id/staff List all staff assigned to a batch with roles
POST /api/v1/admin/batches/:id/staff Assign a user to a batch with a role. Body: { user_id, role, notes? }
PATCH /api/v1/admin/batches/:id/staff/:staffId Update role or notes for a staff member
DELETE /api/v1/admin/batches/:id/staff/:staffId Remove (set is_active=false) a staff member from a batch
GET /api/v1/admin/users?role=trek_leader List users eligible to be assigned as leaders

4.2 Batch status management (Trek Leader / Admin)

Method Path Description
PATCH /api/v1/batches/:id/status Transition batch status. Body: { to_status, note?, location_lat?, location_lng? }
GET /api/v1/batches/:id/status-history Full audit log of all status transitions for a batch
PATCH /api/v1/batches/:id/incident Trek leader posts/updates incident note on a running batch
GET /api/v1/batches/live Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed)
GET /api/v1/batches/:id/live-status Real-time status card for a single batch (polling or SSE)

4.3 Leader-facing mobile routes

These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.

Method Path Description
GET /api/v1/leader/batches Leader's assigned batches: upcoming, live, past
GET /api/v1/leader/batches/:id Full batch detail including participant list, docs status, staff roster
GET /api/v1/leader/batches/:id/participants All confirmed participants with doc/health status and emergency contacts
POST /api/v1/leader/batches/:id/notify Send push notification to all batch participants. Body: { title, message }

4.4 Guide-facing mobile routes

Method Path Description
GET /api/v1/guide/batches Guide's assigned batches
GET /api/v1/guide/batches/:id Batch detail — itinerary, participant count, emergency contacts (no payment data)
GET /api/v1/guide/batches/:id/participants Participant names and emergency contacts only

5. Frontend — Ongoing Treks Feature

5.1 Website: Ongoing Treks section

A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.

Element Data source Behaviour
'Live now' badge batch.status = in_progress Pulsing green dot; visible on trek card in explore listing
'Assembling' badge batch.status = assembling Amber badge — 'Starting soon'
Ongoing Treks section (home) GET /api/v1/batches/live Horizontal scroll card strip; shows trek name, region, day N of M, leader name
Live status card GET /api/v1/batches/:id/live-status (poll 60s) Current status, day number, last status update time, incident note if any
/treks/ongoing page Same endpoint Full grid of all ongoing batches; filterable by region

5.2 Mobile app: Trek Leader screens

A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.

Screen Route Description
Leader home app/(tabs)/leader/index.tsx Cards for each assigned batch: upcoming / live / past
Batch control panel app/(tabs)/leader/batch/[id].tsx Status pill, big action button (context-aware), participant count, staff list
Status transition app/(tabs)/leader/batch/[id]/status.tsx Confirmation sheet with allowed next states, mandatory note input for pause/complete
Participant list app/(tabs)/leader/batch/[id]/participants.tsx Trekker names, doc status indicators, emergency contact tap-to-call
Notify participants app/(tabs)/leader/batch/[id]/notify.tsx Title + message compose; sends push to all confirmed participants
Status history app/(tabs)/leader/batch/[id]/history.tsx Timeline of all status changes with timestamps and notes
Incident log app/(tabs)/leader/batch/[id]/incident.tsx Free-text incident note; visible to admin, not to participants

5.3 Mobile app: Guide screens

Screen Route Description
Guide home app/(tabs)/guide/index.tsx Upcoming and active batch assignments
Batch view app/(tabs)/guide/batch/[id].tsx Read-only: itinerary, participant count, day-wise plan
Participant contacts app/(tabs)/guide/batch/[id]/contacts.tsx Names + emergency contacts (tap-to-call); no booking/payment data

5.4 Participant-facing: ongoing trek card

In the My Treks tab, a booking with an active batch shows an enhanced card:

  • Live status banner: 'Your trek is in progress — Day 3 of 7'
  • Last update timestamp from status_updated_at
  • Guide contact buttons (tap-to-call/WhatsApp for each guide)
  • Trek leader name and emergency contact
  • Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')

6. Loyalty Credit on Trek Completion

6.1 Credit rules

Trek difficulty Base points awarded Notes
Easy 100 pts e.g. Kedarkantha meadows, beginner treks
Moderate 200 pts Most standard Himalayan treks
Difficult 350 pts Technical routes, high-altitude passes
Expedition 500 pts Summit attempts, multi-week treks

Multipliers applied on top of base points:

Multiplier condition Multiplier Example
First completed trek (new user) 1.5× Easy first trek: 100 × 1.5 = 150 pts
Repeat booking (2nd+ trek with OBS) 1.1× Moderate repeat: 200 × 1.1 = 220 pts
Trek completed in current season peak (configurable) 1.2× Peak season bonus
Full group (batch ≥ 90% full) 1.1× Rewarding popular batches
Multipliers stack multiplicatively Repeat + peak: 200 × 1.1 × 1.2 = 264 pts

6.2 BullMQ job: credit_loyalty_on_completion

Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.

Step Action Error handling
1 — Fetch participants Query all bookings WHERE batch_id = X AND status = 'confirmed' If no confirmed bookings: log and exit cleanly
2 — Check idempotency For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin.
3 — Insert pending rows INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee
4 — Compute points For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers Round to nearest integer
5 — Credit points INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry.
6 — Update tier Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed Run per-user after credit. Non-blocking — tier is cosmetic.
7 — Notify Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' Notification failure does not roll back credits
8 — Award certificate Enqueue separate generate_certificate job per user Separate job — idempotent; certificate URL stored on booking

6.3 Points→tier recomputation

Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).

7. Admin Panel Additions

7.1 Batch staff assignment UI

  • On any batch detail page in admin, a 'Staff' tab shows current assignments
  • 'Assign staff' button opens a user search modal; select user → pick role → save
  • Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
  • 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note

7.2 Live batch monitor

New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):

  • Batch name, trek, day N of M, leader name, participant count, last status update
  • Quick-expand to see incident notes and full status history
  • 'Contact leader' button (WhatsApp deep link or in-app message)
  • Admin can force-complete or cancel any running batch from here (with mandatory reason)

7.3 Loyalty audit dashboard

  • Table of all loyalty_pending_credits rows filterable by status
  • 'Failed' rows shown prominently with error_detail and a manual retry button
  • Batch completion summary: which batches have been credited, how many users, total points awarded

8. Migration Plan (Existing Data)

Step Action Notes
1 Run migration: add new columns to trek_batches (status, status_updated_at etc) All existing batches get status = 'scheduled' by default
2 Create new tables: batch_staff, batch_status_history, loyalty_pending_credits No data migration needed — tables start empty
3 Create enum types: trek_batch_status, batch_staff_role, credit_status Do this before altering tables that reference them
4 Migrate existing guide_id FK data For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id)
5 Drop guide_id column from trek_batches After verifying migration in step 4 is complete
6 Backfill status_history Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness
💡
Run all migration steps inside a single database transaction. Use a Prisma migration file with raw SQL for the enum creation (Prisma does not auto-generate enum creation in all cases).

OBS EXPERIENCES

Addendum: Trek Batch Roles, Live Status & Loyalty

Extends MVP Architecture v1.0 | June 2026

1. Overview & Design Principles

Extends MVP Architecture v1.0 | Added June 2026

This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.

Three principles guide the design:

  • Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
  • Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
  • Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.

2. Database Schema Changes

2.1 Updated trek_batches table

Existing columns carry forward. New and changed columns are marked.

Column Type Default Notes
id uuid PK gen_random_uuid() No change
trek_id uuid FK → treks No change
start_date date No change
end_date date No change
price_inr integer No change
seats_total integer No change
seats_booked integer 0 No change
seats_held integer 0 No change
status trek_batch_status (enum) 'scheduled' NEW — see lifecycle §3
status_updated_at timestamptz now() NEW — when status last changed
status_updated_by uuid FK → users null NEW — leader who changed it
started_at timestamptz null NEW — set when status → in_progress
finished_at timestamptz null NEW — set when status → completed
actual_end_date date null NEW — may differ from planned end_date
incident_notes text null NEW — free text, leader-only write
cancellation_reason text null No change
meeting_point text null NEW — e.g. 'Sankri base camp parking'
meeting_time timestamptz null NEW — assembly time day 1
💡
Add a Postgres enum: CREATE TYPE trek_batch_status AS ENUM ('scheduled','assembling','in_progress','paused','completed','cancelled');

2.2 New table: batch_staff

Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE
user_id uuid FK → users Staff must have a user account
role batch_staff_role (enum) trek_leader | guide | coordinator — see §2.3
assigned_by uuid FK → users Admin who made the assignment
assigned_at timestamptz Timestamp of assignment
is_active boolean true = currently on this batch; false = removed/replaced
notes text Internal notes (e.g. 'backup leader', 'local area expert')
💡
Unique constraint: (batch_id, user_id, role) — same person cannot hold the same role twice on one batch. A person CAN be both a guide and coordinator on different batches.
💡
Enforce exactly one trek_leader per active batch at application layer (check before INSERT/UPDATE, not DB constraint, to allow graceful leader handover).

2.3 Enum: batch_staff_role

Role value Who holds it Key permissions
trek_leader 1 per batch (required) Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin
guide 1–N per batch (min 1 required) Read-only access to participant list and docs · View day itinerary · Cannot change status
coordinator 0–N per batch (optional) Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member

2.4 New table: batch_status_history

Append-only audit trail of every status transition. Never update or delete rows.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE (audit trail is batch-scoped)
from_status trek_batch_status null if first transition (scheduled → ...)
to_status trek_batch_status The new status
changed_by uuid FK → users Must be the trek_leader for operational transitions
changed_at timestamptz Server time, not client time
note text Optional leader note at time of transition
location_lat decimal(9,6) Optional GPS at time of status change
location_lng decimal(9,6) Optional GPS at time of status change

2.5 New table: loyalty_pending_credits

Staging table used by the post-completion loyalty job to ensure idempotency.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches
user_id uuid FK → users Trekker who completed the batch
points_to_credit integer Computed at job time from trek difficulty + loyalty rules
status credit_status (enum) pending | processing | credited | failed
job_run_id uuid BullMQ job ID — idempotency key
credited_at timestamptz Set when loyalty_points row is inserted
error_detail text Populated on failure for retry/manual review
💡
Unique constraint on (batch_id, user_id) — guarantees one credit row per trekker per batch regardless of retries.

3. Trek Batch Status Lifecycle

3.1 Status enum values

Status Meaning Who can set it Trigger
scheduled Batch is open for bookings; no trek activity yet System (default on create) Admin creates batch
assembling Bookings closed; participants are gathering at meeting point Trek Leader or Admin Leader taps 'Start assembly' — typically morning of Day 1
in_progress Trek is actively underway Trek Leader only Leader taps 'Begin trek' after headcount at assembly
paused Trek temporarily halted (weather window, medical situation, route issue) Trek Leader only Leader taps 'Pause trek'; must add a note explaining reason
completed Trek finished; all participants safely returned Trek Leader only Leader taps 'Complete trek'; triggers loyalty job
cancelled Batch will not run Admin only (not leader) Admin action; triggers refund workflow

3.2 Allowed transitions

From → → To Allowed? Notes
scheduled assembling Yes Leader or admin; closes new bookings
scheduled cancelled Yes Admin only
assembling in_progress Yes Leader only; sets started_at
assembling cancelled Yes Admin only (e.g. sudden weather)
in_progress paused Yes Leader only; note required
in_progress completed Yes Leader only; sets finished_at, triggers loyalty job
in_progress cancelled No Cannot cancel a running trek — use completed with incident note
paused in_progress Yes Leader only; resumes trek
paused completed Yes Leader only; if trek ends while paused (e.g. emergency extraction)
completed any No Terminal state — no further transitions
cancelled any No Terminal state — no further transitions

3.3 Status transition API

PATCH /api/v1/batches/:id/status

Request body: { to_status, note?, location? }

Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.

Server-side logic on this endpoint:

  • Validate transition is in the allowed matrix above; return 422 if not
  • Check caller is active trek_leader on this batch (or admin)
  • Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
  • INSERT into batch_status_history
  • If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
  • If → in_progress or assembling: send push notification to all confirmed participants
  • Return updated batch object

4. API Routes — New & Updated

4.1 Batch staff management (Admin)

Method Path Description
GET /api/v1/admin/batches/:id/staff List all staff assigned to a batch with roles
POST /api/v1/admin/batches/:id/staff Assign a user to a batch with a role. Body: { user_id, role, notes? }
PATCH /api/v1/admin/batches/:id/staff/:staffId Update role or notes for a staff member
DELETE /api/v1/admin/batches/:id/staff/:staffId Remove (set is_active=false) a staff member from a batch
GET /api/v1/admin/users?role=trek_leader List users eligible to be assigned as leaders

4.2 Batch status management (Trek Leader / Admin)

Method Path Description
PATCH /api/v1/batches/:id/status Transition batch status. Body: { to_status, note?, location_lat?, location_lng? }
GET /api/v1/batches/:id/status-history Full audit log of all status transitions for a batch
PATCH /api/v1/batches/:id/incident Trek leader posts/updates incident note on a running batch
GET /api/v1/batches/live Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed)
GET /api/v1/batches/:id/live-status Real-time status card for a single batch (polling or SSE)

4.3 Leader-facing mobile routes

These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.

Method Path Description
GET /api/v1/leader/batches Leader's assigned batches: upcoming, live, past
GET /api/v1/leader/batches/:id Full batch detail including participant list, docs status, staff roster
GET /api/v1/leader/batches/:id/participants All confirmed participants with doc/health status and emergency contacts
POST /api/v1/leader/batches/:id/notify Send push notification to all batch participants. Body: { title, message }

4.4 Guide-facing mobile routes

Method Path Description
GET /api/v1/guide/batches Guide's assigned batches
GET /api/v1/guide/batches/:id Batch detail — itinerary, participant count, emergency contacts (no payment data)
GET /api/v1/guide/batches/:id/participants Participant names and emergency contacts only

5. Frontend — Ongoing Treks Feature

5.1 Website: Ongoing Treks section

A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.

Element Data source Behaviour
'Live now' badge batch.status = in_progress Pulsing green dot; visible on trek card in explore listing
'Assembling' badge batch.status = assembling Amber badge — 'Starting soon'
Ongoing Treks section (home) GET /api/v1/batches/live Horizontal scroll card strip; shows trek name, region, day N of M, leader name
Live status card GET /api/v1/batches/:id/live-status (poll 60s) Current status, day number, last status update time, incident note if any
/treks/ongoing page Same endpoint Full grid of all ongoing batches; filterable by region

5.2 Mobile app: Trek Leader screens

A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.

Screen Route Description
Leader home app/(tabs)/leader/index.tsx Cards for each assigned batch: upcoming / live / past
Batch control panel app/(tabs)/leader/batch/[id].tsx Status pill, big action button (context-aware), participant count, staff list
Status transition app/(tabs)/leader/batch/[id]/status.tsx Confirmation sheet with allowed next states, mandatory note input for pause/complete
Participant list app/(tabs)/leader/batch/[id]/participants.tsx Trekker names, doc status indicators, emergency contact tap-to-call
Notify participants app/(tabs)/leader/batch/[id]/notify.tsx Title + message compose; sends push to all confirmed participants
Status history app/(tabs)/leader/batch/[id]/history.tsx Timeline of all status changes with timestamps and notes
Incident log app/(tabs)/leader/batch/[id]/incident.tsx Free-text incident note; visible to admin, not to participants

5.3 Mobile app: Guide screens

Screen Route Description
Guide home app/(tabs)/guide/index.tsx Upcoming and active batch assignments
Batch view app/(tabs)/guide/batch/[id].tsx Read-only: itinerary, participant count, day-wise plan
Participant contacts app/(tabs)/guide/batch/[id]/contacts.tsx Names + emergency contacts (tap-to-call); no booking/payment data

5.4 Participant-facing: ongoing trek card

In the My Treks tab, a booking with an active batch shows an enhanced card:

  • Live status banner: 'Your trek is in progress — Day 3 of 7'
  • Last update timestamp from status_updated_at
  • Guide contact buttons (tap-to-call/WhatsApp for each guide)
  • Trek leader name and emergency contact
  • Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')

6. Loyalty Credit on Trek Completion

6.1 Credit rules

Trek difficulty Base points awarded Notes
Easy 100 pts e.g. Kedarkantha meadows, beginner treks
Moderate 200 pts Most standard Himalayan treks
Difficult 350 pts Technical routes, high-altitude passes
Expedition 500 pts Summit attempts, multi-week treks

Multipliers applied on top of base points:

Multiplier condition Multiplier Example
First completed trek (new user) 1.5× Easy first trek: 100 × 1.5 = 150 pts
Repeat booking (2nd+ trek with OBS) 1.1× Moderate repeat: 200 × 1.1 = 220 pts
Trek completed in current season peak (configurable) 1.2× Peak season bonus
Full group (batch ≥ 90% full) 1.1× Rewarding popular batches
Multipliers stack multiplicatively Repeat + peak: 200 × 1.1 × 1.2 = 264 pts

6.2 BullMQ job: credit_loyalty_on_completion

Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.

Step Action Error handling
1 — Fetch participants Query all bookings WHERE batch_id = X AND status = 'confirmed' If no confirmed bookings: log and exit cleanly
2 — Check idempotency For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin.
3 — Insert pending rows INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee
4 — Compute points For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers Round to nearest integer
5 — Credit points INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry.
6 — Update tier Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed Run per-user after credit. Non-blocking — tier is cosmetic.
7 — Notify Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' Notification failure does not roll back credits
8 — Award certificate Enqueue separate generate_certificate job per user Separate job — idempotent; certificate URL stored on booking

6.3 Points→tier recomputation

Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).

7. Admin Panel Additions

7.1 Batch staff assignment UI

  • On any batch detail page in admin, a 'Staff' tab shows current assignments
  • 'Assign staff' button opens a user search modal; select user → pick role → save
  • Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
  • 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note

7.2 Live batch monitor

New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):

  • Batch name, trek, day N of M, leader name, participant count, last status update
  • Quick-expand to see incident notes and full status history
  • 'Contact leader' button (WhatsApp deep link or in-app message)
  • Admin can force-complete or cancel any running batch from here (with mandatory reason)

7.3 Loyalty audit dashboard

  • Table of all loyalty_pending_credits rows filterable by status
  • 'Failed' rows shown prominently with error_detail and a manual retry button
  • Batch completion summary: which batches have been credited, how many users, total points awarded

8. Migration Plan (Existing Data)

Step Action Notes
1 Run migration: add new columns to trek_batches (status, status_updated_at etc) All existing batches get status = 'scheduled' by default
2 Create new tables: batch_staff, batch_status_history, loyalty_pending_credits No data migration needed — tables start empty
3 Create enum types: trek_batch_status, batch_staff_role, credit_status Do this before altering tables that reference them
4 Migrate existing guide_id FK data For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id)
5 Drop guide_id column from trek_batches After verifying migration in step 4 is complete
6 Backfill status_history Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness
💡
Run all migration steps inside a single database transaction. Use a Prisma migration file with raw SQL for the enum creation (Prisma does not auto-generate enum creation in all cases).

OBS EXPERIENCES

Addendum: Trek Batch Roles, Live Status & Loyalty

Extends MVP Architecture v1.0 | June 2026

1. Overview & Design Principles

Extends MVP Architecture v1.0 | Added June 2026

This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.

Three principles guide the design:

  • Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
  • Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
  • Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.

2. Database Schema Changes

2.1 Updated trek_batches table

Existing columns carry forward. New and changed columns are marked.

Column Type Default Notes
id uuid PK gen_random_uuid() No change
trek_id uuid FK → treks No change
start_date date No change
end_date date No change
price_inr integer No change
seats_total integer No change
seats_booked integer 0 No change
seats_held integer 0 No change
status trek_batch_status (enum) 'scheduled' NEW — see lifecycle §3
status_updated_at timestamptz now() NEW — when status last changed
status_updated_by uuid FK → users null NEW — leader who changed it
started_at timestamptz null NEW — set when status → in_progress
finished_at timestamptz null NEW — set when status → completed
actual_end_date date null NEW — may differ from planned end_date
incident_notes text null NEW — free text, leader-only write
cancellation_reason text null No change
meeting_point text null NEW — e.g. 'Sankri base camp parking'
meeting_time timestamptz null NEW — assembly time day 1
💡
Add a Postgres enum: CREATE TYPE trek_batch_status AS ENUM ('scheduled','assembling','in_progress','paused','completed','cancelled');

2.2 New table: batch_staff

Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE
user_id uuid FK → users Staff must have a user account
role batch_staff_role (enum) trek_leader | guide | coordinator — see §2.3
assigned_by uuid FK → users Admin who made the assignment
assigned_at timestamptz Timestamp of assignment
is_active boolean true = currently on this batch; false = removed/replaced
notes text Internal notes (e.g. 'backup leader', 'local area expert')
💡
Unique constraint: (batch_id, user_id, role) — same person cannot hold the same role twice on one batch. A person CAN be both a guide and coordinator on different batches.
💡
Enforce exactly one trek_leader per active batch at application layer (check before INSERT/UPDATE, not DB constraint, to allow graceful leader handover).

2.3 Enum: batch_staff_role

Role value Who holds it Key permissions
trek_leader 1 per batch (required) Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin
guide 1–N per batch (min 1 required) Read-only access to participant list and docs · View day itinerary · Cannot change status
coordinator 0–N per batch (optional) Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member

2.4 New table: batch_status_history

Append-only audit trail of every status transition. Never update or delete rows.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE (audit trail is batch-scoped)
from_status trek_batch_status null if first transition (scheduled → ...)
to_status trek_batch_status The new status
changed_by uuid FK → users Must be the trek_leader for operational transitions
changed_at timestamptz Server time, not client time
note text Optional leader note at time of transition
location_lat decimal(9,6) Optional GPS at time of status change
location_lng decimal(9,6) Optional GPS at time of status change

2.5 New table: loyalty_pending_credits

Staging table used by the post-completion loyalty job to ensure idempotency.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches
user_id uuid FK → users Trekker who completed the batch
points_to_credit integer Computed at job time from trek difficulty + loyalty rules
status credit_status (enum) pending | processing | credited | failed
job_run_id uuid BullMQ job ID — idempotency key
credited_at timestamptz Set when loyalty_points row is inserted
error_detail text Populated on failure for retry/manual review
💡
Unique constraint on (batch_id, user_id) — guarantees one credit row per trekker per batch regardless of retries.

3. Trek Batch Status Lifecycle

3.1 Status enum values

Status Meaning Who can set it Trigger
scheduled Batch is open for bookings; no trek activity yet System (default on create) Admin creates batch
assembling Bookings closed; participants are gathering at meeting point Trek Leader or Admin Leader taps 'Start assembly' — typically morning of Day 1
in_progress Trek is actively underway Trek Leader only Leader taps 'Begin trek' after headcount at assembly
paused Trek temporarily halted (weather window, medical situation, route issue) Trek Leader only Leader taps 'Pause trek'; must add a note explaining reason
completed Trek finished; all participants safely returned Trek Leader only Leader taps 'Complete trek'; triggers loyalty job
cancelled Batch will not run Admin only (not leader) Admin action; triggers refund workflow

3.2 Allowed transitions

From → → To Allowed? Notes
scheduled assembling Yes Leader or admin; closes new bookings
scheduled cancelled Yes Admin only
assembling in_progress Yes Leader only; sets started_at
assembling cancelled Yes Admin only (e.g. sudden weather)
in_progress paused Yes Leader only; note required
in_progress completed Yes Leader only; sets finished_at, triggers loyalty job
in_progress cancelled No Cannot cancel a running trek — use completed with incident note
paused in_progress Yes Leader only; resumes trek
paused completed Yes Leader only; if trek ends while paused (e.g. emergency extraction)
completed any No Terminal state — no further transitions
cancelled any No Terminal state — no further transitions

3.3 Status transition API

PATCH /api/v1/batches/:id/status

Request body: { to_status, note?, location? }

Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.

Server-side logic on this endpoint:

  • Validate transition is in the allowed matrix above; return 422 if not
  • Check caller is active trek_leader on this batch (or admin)
  • Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
  • INSERT into batch_status_history
  • If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
  • If → in_progress or assembling: send push notification to all confirmed participants
  • Return updated batch object

4. API Routes — New & Updated

4.1 Batch staff management (Admin)

Method Path Description
GET /api/v1/admin/batches/:id/staff List all staff assigned to a batch with roles
POST /api/v1/admin/batches/:id/staff Assign a user to a batch with a role. Body: { user_id, role, notes? }
PATCH /api/v1/admin/batches/:id/staff/:staffId Update role or notes for a staff member
DELETE /api/v1/admin/batches/:id/staff/:staffId Remove (set is_active=false) a staff member from a batch
GET /api/v1/admin/users?role=trek_leader List users eligible to be assigned as leaders

4.2 Batch status management (Trek Leader / Admin)

Method Path Description
PATCH /api/v1/batches/:id/status Transition batch status. Body: { to_status, note?, location_lat?, location_lng? }
GET /api/v1/batches/:id/status-history Full audit log of all status transitions for a batch
PATCH /api/v1/batches/:id/incident Trek leader posts/updates incident note on a running batch
GET /api/v1/batches/live Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed)
GET /api/v1/batches/:id/live-status Real-time status card for a single batch (polling or SSE)

4.3 Leader-facing mobile routes

These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.

Method Path Description
GET /api/v1/leader/batches Leader's assigned batches: upcoming, live, past
GET /api/v1/leader/batches/:id Full batch detail including participant list, docs status, staff roster
GET /api/v1/leader/batches/:id/participants All confirmed participants with doc/health status and emergency contacts
POST /api/v1/leader/batches/:id/notify Send push notification to all batch participants. Body: { title, message }

4.4 Guide-facing mobile routes

Method Path Description
GET /api/v1/guide/batches Guide's assigned batches
GET /api/v1/guide/batches/:id Batch detail — itinerary, participant count, emergency contacts (no payment data)
GET /api/v1/guide/batches/:id/participants Participant names and emergency contacts only

5. Frontend — Ongoing Treks Feature

5.1 Website: Ongoing Treks section

A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.

Element Data source Behaviour
'Live now' badge batch.status = in_progress Pulsing green dot; visible on trek card in explore listing
'Assembling' badge batch.status = assembling Amber badge — 'Starting soon'
Ongoing Treks section (home) GET /api/v1/batches/live Horizontal scroll card strip; shows trek name, region, day N of M, leader name
Live status card GET /api/v1/batches/:id/live-status (poll 60s) Current status, day number, last status update time, incident note if any
/treks/ongoing page Same endpoint Full grid of all ongoing batches; filterable by region

5.2 Mobile app: Trek Leader screens

A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.

Screen Route Description
Leader home app/(tabs)/leader/index.tsx Cards for each assigned batch: upcoming / live / past
Batch control panel app/(tabs)/leader/batch/[id].tsx Status pill, big action button (context-aware), participant count, staff list
Status transition app/(tabs)/leader/batch/[id]/status.tsx Confirmation sheet with allowed next states, mandatory note input for pause/complete
Participant list app/(tabs)/leader/batch/[id]/participants.tsx Trekker names, doc status indicators, emergency contact tap-to-call
Notify participants app/(tabs)/leader/batch/[id]/notify.tsx Title + message compose; sends push to all confirmed participants
Status history app/(tabs)/leader/batch/[id]/history.tsx Timeline of all status changes with timestamps and notes
Incident log app/(tabs)/leader/batch/[id]/incident.tsx Free-text incident note; visible to admin, not to participants

5.3 Mobile app: Guide screens

Screen Route Description
Guide home app/(tabs)/guide/index.tsx Upcoming and active batch assignments
Batch view app/(tabs)/guide/batch/[id].tsx Read-only: itinerary, participant count, day-wise plan
Participant contacts app/(tabs)/guide/batch/[id]/contacts.tsx Names + emergency contacts (tap-to-call); no booking/payment data

5.4 Participant-facing: ongoing trek card

In the My Treks tab, a booking with an active batch shows an enhanced card:

  • Live status banner: 'Your trek is in progress — Day 3 of 7'
  • Last update timestamp from status_updated_at
  • Guide contact buttons (tap-to-call/WhatsApp for each guide)
  • Trek leader name and emergency contact
  • Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')

6. Loyalty Credit on Trek Completion

6.1 Credit rules

Trek difficulty Base points awarded Notes
Easy 100 pts e.g. Kedarkantha meadows, beginner treks
Moderate 200 pts Most standard Himalayan treks
Difficult 350 pts Technical routes, high-altitude passes
Expedition 500 pts Summit attempts, multi-week treks

Multipliers applied on top of base points:

Multiplier condition Multiplier Example
First completed trek (new user) 1.5× Easy first trek: 100 × 1.5 = 150 pts
Repeat booking (2nd+ trek with OBS) 1.1× Moderate repeat: 200 × 1.1 = 220 pts
Trek completed in current season peak (configurable) 1.2× Peak season bonus
Full group (batch ≥ 90% full) 1.1× Rewarding popular batches
Multipliers stack multiplicatively Repeat + peak: 200 × 1.1 × 1.2 = 264 pts

6.2 BullMQ job: credit_loyalty_on_completion

Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.

Step Action Error handling
1 — Fetch participants Query all bookings WHERE batch_id = X AND status = 'confirmed' If no confirmed bookings: log and exit cleanly
2 — Check idempotency For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin.
3 — Insert pending rows INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee
4 — Compute points For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers Round to nearest integer
5 — Credit points INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry.
6 — Update tier Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed Run per-user after credit. Non-blocking — tier is cosmetic.
7 — Notify Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' Notification failure does not roll back credits
8 — Award certificate Enqueue separate generate_certificate job per user Separate job — idempotent; certificate URL stored on booking

6.3 Points→tier recomputation

Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).

7. Admin Panel Additions

7.1 Batch staff assignment UI

  • On any batch detail page in admin, a 'Staff' tab shows current assignments
  • 'Assign staff' button opens a user search modal; select user → pick role → save
  • Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
  • 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note

7.2 Live batch monitor

New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):

  • Batch name, trek, day N of M, leader name, participant count, last status update
  • Quick-expand to see incident notes and full status history
  • 'Contact leader' button (WhatsApp deep link or in-app message)
  • Admin can force-complete or cancel any running batch from here (with mandatory reason)

7.3 Loyalty audit dashboard

  • Table of all loyalty_pending_credits rows filterable by status
  • 'Failed' rows shown prominently with error_detail and a manual retry button
  • Batch completion summary: which batches have been credited, how many users, total points awarded

8. Migration Plan (Existing Data)

Step Action Notes
1 Run migration: add new columns to trek_batches (status, status_updated_at etc) All existing batches get status = 'scheduled' by default
2 Create new tables: batch_staff, batch_status_history, loyalty_pending_credits No data migration needed — tables start empty
3 Create enum types: trek_batch_status, batch_staff_role, credit_status Do this before altering tables that reference them
4 Migrate existing guide_id FK data For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id)
5 Drop guide_id column from trek_batches After verifying migration in step 4 is complete
6 Backfill status_history Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness
💡
Run all migration steps inside a single database transaction. Use a Prisma migration file with raw SQL for the enum creation (Prisma does not auto-generate enum creation in all cases).

OBS EXPERIENCES

Addendum: Trek Batch Roles, Live Status & Loyalty

Extends MVP Architecture v1.0 | June 2026

1. Overview & Design Principles

Extends MVP Architecture v1.0 | Added June 2026

This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.

Three principles guide the design:

  • Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
  • Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
  • Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.

2. Database Schema Changes

2.1 Updated trek_batches table

Existing columns carry forward. New and changed columns are marked.

Column Type Default Notes
id uuid PK gen_random_uuid() No change
trek_id uuid FK → treks No change
start_date date No change
end_date date No change
price_inr integer No change
seats_total integer No change
seats_booked integer 0 No change
seats_held integer 0 No change
status trek_batch_status (enum) 'scheduled' NEW — see lifecycle §3
status_updated_at timestamptz now() NEW — when status last changed
status_updated_by uuid FK → users null NEW — leader who changed it
started_at timestamptz null NEW — set when status → in_progress
finished_at timestamptz null NEW — set when status → completed
actual_end_date date null NEW — may differ from planned end_date
incident_notes text null NEW — free text, leader-only write
cancellation_reason text null No change
meeting_point text null NEW — e.g. 'Sankri base camp parking'
meeting_time timestamptz null NEW — assembly time day 1
💡
Add a Postgres enum: CREATE TYPE trek_batch_status AS ENUM ('scheduled','assembling','in_progress','paused','completed','cancelled');

2.2 New table: batch_staff

Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE
user_id uuid FK → users Staff must have a user account
role batch_staff_role (enum) trek_leader | guide | coordinator — see §2.3
assigned_by uuid FK → users Admin who made the assignment
assigned_at timestamptz Timestamp of assignment
is_active boolean true = currently on this batch; false = removed/replaced
notes text Internal notes (e.g. 'backup leader', 'local area expert')
💡
Unique constraint: (batch_id, user_id, role) — same person cannot hold the same role twice on one batch. A person CAN be both a guide and coordinator on different batches.
💡
Enforce exactly one trek_leader per active batch at application layer (check before INSERT/UPDATE, not DB constraint, to allow graceful leader handover).

2.3 Enum: batch_staff_role

Role value Who holds it Key permissions
trek_leader 1 per batch (required) Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin
guide 1–N per batch (min 1 required) Read-only access to participant list and docs · View day itinerary · Cannot change status
coordinator 0–N per batch (optional) Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member

2.4 New table: batch_status_history

Append-only audit trail of every status transition. Never update or delete rows.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE (audit trail is batch-scoped)
from_status trek_batch_status null if first transition (scheduled → ...)
to_status trek_batch_status The new status
changed_by uuid FK → users Must be the trek_leader for operational transitions
changed_at timestamptz Server time, not client time
note text Optional leader note at time of transition
location_lat decimal(9,6) Optional GPS at time of status change
location_lng decimal(9,6) Optional GPS at time of status change

2.5 New table: loyalty_pending_credits

Staging table used by the post-completion loyalty job to ensure idempotency.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches
user_id uuid FK → users Trekker who completed the batch
points_to_credit integer Computed at job time from trek difficulty + loyalty rules
status credit_status (enum) pending | processing | credited | failed
job_run_id uuid BullMQ job ID — idempotency key
credited_at timestamptz Set when loyalty_points row is inserted
error_detail text Populated on failure for retry/manual review
💡
Unique constraint on (batch_id, user_id) — guarantees one credit row per trekker per batch regardless of retries.

3. Trek Batch Status Lifecycle

3.1 Status enum values

Status Meaning Who can set it Trigger
scheduled Batch is open for bookings; no trek activity yet System (default on create) Admin creates batch
assembling Bookings closed; participants are gathering at meeting point Trek Leader or Admin Leader taps 'Start assembly' — typically morning of Day 1
in_progress Trek is actively underway Trek Leader only Leader taps 'Begin trek' after headcount at assembly
paused Trek temporarily halted (weather window, medical situation, route issue) Trek Leader only Leader taps 'Pause trek'; must add a note explaining reason
completed Trek finished; all participants safely returned Trek Leader only Leader taps 'Complete trek'; triggers loyalty job
cancelled Batch will not run Admin only (not leader) Admin action; triggers refund workflow

3.2 Allowed transitions

From → → To Allowed? Notes
scheduled assembling Yes Leader or admin; closes new bookings
scheduled cancelled Yes Admin only
assembling in_progress Yes Leader only; sets started_at
assembling cancelled Yes Admin only (e.g. sudden weather)
in_progress paused Yes Leader only; note required
in_progress completed Yes Leader only; sets finished_at, triggers loyalty job
in_progress cancelled No Cannot cancel a running trek — use completed with incident note
paused in_progress Yes Leader only; resumes trek
paused completed Yes Leader only; if trek ends while paused (e.g. emergency extraction)
completed any No Terminal state — no further transitions
cancelled any No Terminal state — no further transitions

3.3 Status transition API

PATCH /api/v1/batches/:id/status

Request body: { to_status, note?, location? }

Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.

Server-side logic on this endpoint:

  • Validate transition is in the allowed matrix above; return 422 if not
  • Check caller is active trek_leader on this batch (or admin)
  • Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
  • INSERT into batch_status_history
  • If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
  • If → in_progress or assembling: send push notification to all confirmed participants
  • Return updated batch object

4. API Routes — New & Updated

4.1 Batch staff management (Admin)

Method Path Description
GET /api/v1/admin/batches/:id/staff List all staff assigned to a batch with roles
POST /api/v1/admin/batches/:id/staff Assign a user to a batch with a role. Body: { user_id, role, notes? }
PATCH /api/v1/admin/batches/:id/staff/:staffId Update role or notes for a staff member
DELETE /api/v1/admin/batches/:id/staff/:staffId Remove (set is_active=false) a staff member from a batch
GET /api/v1/admin/users?role=trek_leader List users eligible to be assigned as leaders

4.2 Batch status management (Trek Leader / Admin)

Method Path Description
PATCH /api/v1/batches/:id/status Transition batch status. Body: { to_status, note?, location_lat?, location_lng? }
GET /api/v1/batches/:id/status-history Full audit log of all status transitions for a batch
PATCH /api/v1/batches/:id/incident Trek leader posts/updates incident note on a running batch
GET /api/v1/batches/live Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed)
GET /api/v1/batches/:id/live-status Real-time status card for a single batch (polling or SSE)

4.3 Leader-facing mobile routes

These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.

Method Path Description
GET /api/v1/leader/batches Leader's assigned batches: upcoming, live, past
GET /api/v1/leader/batches/:id Full batch detail including participant list, docs status, staff roster
GET /api/v1/leader/batches/:id/participants All confirmed participants with doc/health status and emergency contacts
POST /api/v1/leader/batches/:id/notify Send push notification to all batch participants. Body: { title, message }

4.4 Guide-facing mobile routes

Method Path Description
GET /api/v1/guide/batches Guide's assigned batches
GET /api/v1/guide/batches/:id Batch detail — itinerary, participant count, emergency contacts (no payment data)
GET /api/v1/guide/batches/:id/participants Participant names and emergency contacts only

5. Frontend — Ongoing Treks Feature

5.1 Website: Ongoing Treks section

A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.

Element Data source Behaviour
'Live now' badge batch.status = in_progress Pulsing green dot; visible on trek card in explore listing
'Assembling' badge batch.status = assembling Amber badge — 'Starting soon'
Ongoing Treks section (home) GET /api/v1/batches/live Horizontal scroll card strip; shows trek name, region, day N of M, leader name
Live status card GET /api/v1/batches/:id/live-status (poll 60s) Current status, day number, last status update time, incident note if any
/treks/ongoing page Same endpoint Full grid of all ongoing batches; filterable by region

5.2 Mobile app: Trek Leader screens

A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.

Screen Route Description
Leader home app/(tabs)/leader/index.tsx Cards for each assigned batch: upcoming / live / past
Batch control panel app/(tabs)/leader/batch/[id].tsx Status pill, big action button (context-aware), participant count, staff list
Status transition app/(tabs)/leader/batch/[id]/status.tsx Confirmation sheet with allowed next states, mandatory note input for pause/complete
Participant list app/(tabs)/leader/batch/[id]/participants.tsx Trekker names, doc status indicators, emergency contact tap-to-call
Notify participants app/(tabs)/leader/batch/[id]/notify.tsx Title + message compose; sends push to all confirmed participants
Status history app/(tabs)/leader/batch/[id]/history.tsx Timeline of all status changes with timestamps and notes
Incident log app/(tabs)/leader/batch/[id]/incident.tsx Free-text incident note; visible to admin, not to participants

5.3 Mobile app: Guide screens

Screen Route Description
Guide home app/(tabs)/guide/index.tsx Upcoming and active batch assignments
Batch view app/(tabs)/guide/batch/[id].tsx Read-only: itinerary, participant count, day-wise plan
Participant contacts app/(tabs)/guide/batch/[id]/contacts.tsx Names + emergency contacts (tap-to-call); no booking/payment data

5.4 Participant-facing: ongoing trek card

In the My Treks tab, a booking with an active batch shows an enhanced card:

  • Live status banner: 'Your trek is in progress — Day 3 of 7'
  • Last update timestamp from status_updated_at
  • Guide contact buttons (tap-to-call/WhatsApp for each guide)
  • Trek leader name and emergency contact
  • Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')

6. Loyalty Credit on Trek Completion

6.1 Credit rules

Trek difficulty Base points awarded Notes
Easy 100 pts e.g. Kedarkantha meadows, beginner treks
Moderate 200 pts Most standard Himalayan treks
Difficult 350 pts Technical routes, high-altitude passes
Expedition 500 pts Summit attempts, multi-week treks

Multipliers applied on top of base points:

Multiplier condition Multiplier Example
First completed trek (new user) 1.5× Easy first trek: 100 × 1.5 = 150 pts
Repeat booking (2nd+ trek with OBS) 1.1× Moderate repeat: 200 × 1.1 = 220 pts
Trek completed in current season peak (configurable) 1.2× Peak season bonus
Full group (batch ≥ 90% full) 1.1× Rewarding popular batches
Multipliers stack multiplicatively Repeat + peak: 200 × 1.1 × 1.2 = 264 pts

6.2 BullMQ job: credit_loyalty_on_completion

Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.

Step Action Error handling
1 — Fetch participants Query all bookings WHERE batch_id = X AND status = 'confirmed' If no confirmed bookings: log and exit cleanly
2 — Check idempotency For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin.
3 — Insert pending rows INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee
4 — Compute points For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers Round to nearest integer
5 — Credit points INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry.
6 — Update tier Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed Run per-user after credit. Non-blocking — tier is cosmetic.
7 — Notify Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' Notification failure does not roll back credits
8 — Award certificate Enqueue separate generate_certificate job per user Separate job — idempotent; certificate URL stored on booking

6.3 Points→tier recomputation

Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).

7. Admin Panel Additions

7.1 Batch staff assignment UI

  • On any batch detail page in admin, a 'Staff' tab shows current assignments
  • 'Assign staff' button opens a user search modal; select user → pick role → save
  • Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
  • 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note

7.2 Live batch monitor

New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):

  • Batch name, trek, day N of M, leader name, participant count, last status update
  • Quick-expand to see incident notes and full status history
  • 'Contact leader' button (WhatsApp deep link or in-app message)
  • Admin can force-complete or cancel any running batch from here (with mandatory reason)

7.3 Loyalty audit dashboard

  • Table of all loyalty_pending_credits rows filterable by status
  • 'Failed' rows shown prominently with error_detail and a manual retry button
  • Batch completion summary: which batches have been credited, how many users, total points awarded

8. Migration Plan (Existing Data)

Step Action Notes
1 Run migration: add new columns to trek_batches (status, status_updated_at etc) All existing batches get status = 'scheduled' by default
2 Create new tables: batch_staff, batch_status_history, loyalty_pending_credits No data migration needed — tables start empty
3 Create enum types: trek_batch_status, batch_staff_role, credit_status Do this before altering tables that reference them
4 Migrate existing guide_id FK data For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id)
5 Drop guide_id column from trek_batches After verifying migration in step 4 is complete
6 Backfill status_history Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness
💡
Run all migration steps inside a single database transaction. Use a Prisma migration file with raw SQL for the enum creation (Prisma does not auto-generate enum creation in all cases).

OBS EXPERIENCES

Addendum: Trek Batch Roles, Live Status & Loyalty

Extends MVP Architecture v1.0 | June 2026

1. Overview & Design Principles

Extends MVP Architecture v1.0 | Added June 2026

This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.

Three principles guide the design:

  • Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
  • Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
  • Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.

2. Database Schema Changes

2.1 Updated trek_batches table

Existing columns carry forward. New and changed columns are marked.

Column Type Default Notes
id uuid PK gen_random_uuid() No change
trek_id uuid FK → treks No change
start_date date No change
end_date date No change
price_inr integer No change
seats_total integer No change
seats_booked integer 0 No change
seats_held integer 0 No change
status trek_batch_status (enum) 'scheduled' NEW — see lifecycle §3
status_updated_at timestamptz now() NEW — when status last changed
status_updated_by uuid FK → users null NEW — leader who changed it
started_at timestamptz null NEW — set when status → in_progress
finished_at timestamptz null NEW — set when status → completed
actual_end_date date null NEW — may differ from planned end_date
incident_notes text null NEW — free text, leader-only write
cancellation_reason text null No change
meeting_point text null NEW — e.g. 'Sankri base camp parking'
meeting_time timestamptz null NEW — assembly time day 1
💡
Add a Postgres enum: CREATE TYPE trek_batch_status AS ENUM ('scheduled','assembling','in_progress','paused','completed','cancelled');

2.2 New table: batch_staff

Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE
user_id uuid FK → users Staff must have a user account
role batch_staff_role (enum) trek_leader | guide | coordinator — see §2.3
assigned_by uuid FK → users Admin who made the assignment
assigned_at timestamptz Timestamp of assignment
is_active boolean true = currently on this batch; false = removed/replaced
notes text Internal notes (e.g. 'backup leader', 'local area expert')
💡
Unique constraint: (batch_id, user_id, role) — same person cannot hold the same role twice on one batch. A person CAN be both a guide and coordinator on different batches.
💡
Enforce exactly one trek_leader per active batch at application layer (check before INSERT/UPDATE, not DB constraint, to allow graceful leader handover).

2.3 Enum: batch_staff_role

Role value Who holds it Key permissions
trek_leader 1 per batch (required) Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin
guide 1–N per batch (min 1 required) Read-only access to participant list and docs · View day itinerary · Cannot change status
coordinator 0–N per batch (optional) Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member

2.4 New table: batch_status_history

Append-only audit trail of every status transition. Never update or delete rows.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE (audit trail is batch-scoped)
from_status trek_batch_status null if first transition (scheduled → ...)
to_status trek_batch_status The new status
changed_by uuid FK → users Must be the trek_leader for operational transitions
changed_at timestamptz Server time, not client time
note text Optional leader note at time of transition
location_lat decimal(9,6) Optional GPS at time of status change
location_lng decimal(9,6) Optional GPS at time of status change

2.5 New table: loyalty_pending_credits

Staging table used by the post-completion loyalty job to ensure idempotency.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches
user_id uuid FK → users Trekker who completed the batch
points_to_credit integer Computed at job time from trek difficulty + loyalty rules
status credit_status (enum) pending | processing | credited | failed
job_run_id uuid BullMQ job ID — idempotency key
credited_at timestamptz Set when loyalty_points row is inserted
error_detail text Populated on failure for retry/manual review
💡
Unique constraint on (batch_id, user_id) — guarantees one credit row per trekker per batch regardless of retries.

3. Trek Batch Status Lifecycle

3.1 Status enum values

Status Meaning Who can set it Trigger
scheduled Batch is open for bookings; no trek activity yet System (default on create) Admin creates batch
assembling Bookings closed; participants are gathering at meeting point Trek Leader or Admin Leader taps 'Start assembly' — typically morning of Day 1
in_progress Trek is actively underway Trek Leader only Leader taps 'Begin trek' after headcount at assembly
paused Trek temporarily halted (weather window, medical situation, route issue) Trek Leader only Leader taps 'Pause trek'; must add a note explaining reason
completed Trek finished; all participants safely returned Trek Leader only Leader taps 'Complete trek'; triggers loyalty job
cancelled Batch will not run Admin only (not leader) Admin action; triggers refund workflow

3.2 Allowed transitions

From → → To Allowed? Notes
scheduled assembling Yes Leader or admin; closes new bookings
scheduled cancelled Yes Admin only
assembling in_progress Yes Leader only; sets started_at
assembling cancelled Yes Admin only (e.g. sudden weather)
in_progress paused Yes Leader only; note required
in_progress completed Yes Leader only; sets finished_at, triggers loyalty job
in_progress cancelled No Cannot cancel a running trek — use completed with incident note
paused in_progress Yes Leader only; resumes trek
paused completed Yes Leader only; if trek ends while paused (e.g. emergency extraction)
completed any No Terminal state — no further transitions
cancelled any No Terminal state — no further transitions

3.3 Status transition API

PATCH /api/v1/batches/:id/status

Request body: { to_status, note?, location? }

Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.

Server-side logic on this endpoint:

  • Validate transition is in the allowed matrix above; return 422 if not
  • Check caller is active trek_leader on this batch (or admin)
  • Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
  • INSERT into batch_status_history
  • If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
  • If → in_progress or assembling: send push notification to all confirmed participants
  • Return updated batch object

4. API Routes — New & Updated

4.1 Batch staff management (Admin)

Method Path Description
GET /api/v1/admin/batches/:id/staff List all staff assigned to a batch with roles
POST /api/v1/admin/batches/:id/staff Assign a user to a batch with a role. Body: { user_id, role, notes? }
PATCH /api/v1/admin/batches/:id/staff/:staffId Update role or notes for a staff member
DELETE /api/v1/admin/batches/:id/staff/:staffId Remove (set is_active=false) a staff member from a batch
GET /api/v1/admin/users?role=trek_leader List users eligible to be assigned as leaders

4.2 Batch status management (Trek Leader / Admin)

Method Path Description
PATCH /api/v1/batches/:id/status Transition batch status. Body: { to_status, note?, location_lat?, location_lng? }
GET /api/v1/batches/:id/status-history Full audit log of all status transitions for a batch
PATCH /api/v1/batches/:id/incident Trek leader posts/updates incident note on a running batch
GET /api/v1/batches/live Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed)
GET /api/v1/batches/:id/live-status Real-time status card for a single batch (polling or SSE)

4.3 Leader-facing mobile routes

These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.

Method Path Description
GET /api/v1/leader/batches Leader's assigned batches: upcoming, live, past
GET /api/v1/leader/batches/:id Full batch detail including participant list, docs status, staff roster
GET /api/v1/leader/batches/:id/participants All confirmed participants with doc/health status and emergency contacts
POST /api/v1/leader/batches/:id/notify Send push notification to all batch participants. Body: { title, message }

4.4 Guide-facing mobile routes

Method Path Description
GET /api/v1/guide/batches Guide's assigned batches
GET /api/v1/guide/batches/:id Batch detail — itinerary, participant count, emergency contacts (no payment data)
GET /api/v1/guide/batches/:id/participants Participant names and emergency contacts only

5. Frontend — Ongoing Treks Feature

5.1 Website: Ongoing Treks section

A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.

Element Data source Behaviour
'Live now' badge batch.status = in_progress Pulsing green dot; visible on trek card in explore listing
'Assembling' badge batch.status = assembling Amber badge — 'Starting soon'
Ongoing Treks section (home) GET /api/v1/batches/live Horizontal scroll card strip; shows trek name, region, day N of M, leader name
Live status card GET /api/v1/batches/:id/live-status (poll 60s) Current status, day number, last status update time, incident note if any
/treks/ongoing page Same endpoint Full grid of all ongoing batches; filterable by region

5.2 Mobile app: Trek Leader screens

A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.

Screen Route Description
Leader home app/(tabs)/leader/index.tsx Cards for each assigned batch: upcoming / live / past
Batch control panel app/(tabs)/leader/batch/[id].tsx Status pill, big action button (context-aware), participant count, staff list
Status transition app/(tabs)/leader/batch/[id]/status.tsx Confirmation sheet with allowed next states, mandatory note input for pause/complete
Participant list app/(tabs)/leader/batch/[id]/participants.tsx Trekker names, doc status indicators, emergency contact tap-to-call
Notify participants app/(tabs)/leader/batch/[id]/notify.tsx Title + message compose; sends push to all confirmed participants
Status history app/(tabs)/leader/batch/[id]/history.tsx Timeline of all status changes with timestamps and notes
Incident log app/(tabs)/leader/batch/[id]/incident.tsx Free-text incident note; visible to admin, not to participants

5.3 Mobile app: Guide screens

Screen Route Description
Guide home app/(tabs)/guide/index.tsx Upcoming and active batch assignments
Batch view app/(tabs)/guide/batch/[id].tsx Read-only: itinerary, participant count, day-wise plan
Participant contacts app/(tabs)/guide/batch/[id]/contacts.tsx Names + emergency contacts (tap-to-call); no booking/payment data

5.4 Participant-facing: ongoing trek card

In the My Treks tab, a booking with an active batch shows an enhanced card:

  • Live status banner: 'Your trek is in progress — Day 3 of 7'
  • Last update timestamp from status_updated_at
  • Guide contact buttons (tap-to-call/WhatsApp for each guide)
  • Trek leader name and emergency contact
  • Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')

6. Loyalty Credit on Trek Completion

6.1 Credit rules

Trek difficulty Base points awarded Notes
Easy 100 pts e.g. Kedarkantha meadows, beginner treks
Moderate 200 pts Most standard Himalayan treks
Difficult 350 pts Technical routes, high-altitude passes
Expedition 500 pts Summit attempts, multi-week treks

Multipliers applied on top of base points:

Multiplier condition Multiplier Example
First completed trek (new user) 1.5× Easy first trek: 100 × 1.5 = 150 pts
Repeat booking (2nd+ trek with OBS) 1.1× Moderate repeat: 200 × 1.1 = 220 pts
Trek completed in current season peak (configurable) 1.2× Peak season bonus
Full group (batch ≥ 90% full) 1.1× Rewarding popular batches
Multipliers stack multiplicatively Repeat + peak: 200 × 1.1 × 1.2 = 264 pts

6.2 BullMQ job: credit_loyalty_on_completion

Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.

Step Action Error handling
1 — Fetch participants Query all bookings WHERE batch_id = X AND status = 'confirmed' If no confirmed bookings: log and exit cleanly
2 — Check idempotency For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin.
3 — Insert pending rows INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee
4 — Compute points For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers Round to nearest integer
5 — Credit points INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry.
6 — Update tier Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed Run per-user after credit. Non-blocking — tier is cosmetic.
7 — Notify Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' Notification failure does not roll back credits
8 — Award certificate Enqueue separate generate_certificate job per user Separate job — idempotent; certificate URL stored on booking

6.3 Points→tier recomputation

Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).

7. Admin Panel Additions

7.1 Batch staff assignment UI

  • On any batch detail page in admin, a 'Staff' tab shows current assignments
  • 'Assign staff' button opens a user search modal; select user → pick role → save
  • Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
  • 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note

7.2 Live batch monitor

New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):

  • Batch name, trek, day N of M, leader name, participant count, last status update
  • Quick-expand to see incident notes and full status history
  • 'Contact leader' button (WhatsApp deep link or in-app message)
  • Admin can force-complete or cancel any running batch from here (with mandatory reason)

7.3 Loyalty audit dashboard

  • Table of all loyalty_pending_credits rows filterable by status
  • 'Failed' rows shown prominently with error_detail and a manual retry button
  • Batch completion summary: which batches have been credited, how many users, total points awarded

8. Migration Plan (Existing Data)

Step Action Notes
1 Run migration: add new columns to trek_batches (status, status_updated_at etc) All existing batches get status = 'scheduled' by default
2 Create new tables: batch_staff, batch_status_history, loyalty_pending_credits No data migration needed — tables start empty
3 Create enum types: trek_batch_status, batch_staff_role, credit_status Do this before altering tables that reference them
4 Migrate existing guide_id FK data For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id)
5 Drop guide_id column from trek_batches After verifying migration in step 4 is complete
6 Backfill status_history Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness
💡
Run all migration steps inside a single database transaction. Use a Prisma migration file with raw SQL for the enum creation (Prisma does not auto-generate enum creation in all cases).

OBS EXPERIENCES

Addendum: Trek Batch Roles, Live Status & Loyalty

Extends MVP Architecture v1.0 | June 2026

1. Overview & Design Principles

Extends MVP Architecture v1.0 | Added June 2026

This addendum extends the OBS MVP architecture to support multi-role batch staffing, a full trek lifecycle with status transitions, live 'ongoing treks' visibility on the frontend, and automated loyalty point crediting on batch completion.

Three principles guide the design:

  • Single source of truth — batch status lives in one column on trek_batches; all derived state (frontend cards, loyalty jobs, notifications) reads from it.
  • Role separation — Trek Leader owns operational authority (status transitions, incident logging); Guides are operational staff; Coordinators are back-office support. Users see none of this complexity.
  • Idempotent loyalty — points are credited by a background job keyed on batch_id + user_id, so retries never double-credit.

2. Database Schema Changes

2.1 Updated trek_batches table

Existing columns carry forward. New and changed columns are marked.

Column Type Default Notes
id uuid PK gen_random_uuid() No change
trek_id uuid FK → treks No change
start_date date No change
end_date date No change
price_inr integer No change
seats_total integer No change
seats_booked integer 0 No change
seats_held integer 0 No change
status trek_batch_status (enum) 'scheduled' NEW — see lifecycle §3
status_updated_at timestamptz now() NEW — when status last changed
status_updated_by uuid FK → users null NEW — leader who changed it
started_at timestamptz null NEW — set when status → in_progress
finished_at timestamptz null NEW — set when status → completed
actual_end_date date null NEW — may differ from planned end_date
incident_notes text null NEW — free text, leader-only write
cancellation_reason text null No change
meeting_point text null NEW — e.g. 'Sankri base camp parking'
meeting_time timestamptz null NEW — assembly time day 1
💡
Add a Postgres enum: CREATE TYPE trek_batch_status AS ENUM ('scheduled','assembling','in_progress','paused','completed','cancelled');

2.2 New table: batch_staff

Replaces the old single guide_id FK on trek_batches. One batch can have multiple staff with distinct roles.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE
user_id uuid FK → users Staff must have a user account
role batch_staff_role (enum) trek_leader | guide | coordinator — see §2.3
assigned_by uuid FK → users Admin who made the assignment
assigned_at timestamptz Timestamp of assignment
is_active boolean true = currently on this batch; false = removed/replaced
notes text Internal notes (e.g. 'backup leader', 'local area expert')
💡
Unique constraint: (batch_id, user_id, role) — same person cannot hold the same role twice on one batch. A person CAN be both a guide and coordinator on different batches.
💡
Enforce exactly one trek_leader per active batch at application layer (check before INSERT/UPDATE, not DB constraint, to allow graceful leader handover).

2.3 Enum: batch_staff_role

Role value Who holds it Key permissions
trek_leader 1 per batch (required) Can change batch status · Post incident notes · View all participant docs · Send batch-wide push notifications · Access group chat as admin
guide 1–N per batch (min 1 required) Read-only access to participant list and docs · View day itinerary · Cannot change status
coordinator 0–N per batch (optional) Back-office role: view booking details, payment status, document approval queue · Cannot change status · Typically an OBS ops staff member

2.4 New table: batch_status_history

Append-only audit trail of every status transition. Never update or delete rows.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches CASCADE DELETE (audit trail is batch-scoped)
from_status trek_batch_status null if first transition (scheduled → ...)
to_status trek_batch_status The new status
changed_by uuid FK → users Must be the trek_leader for operational transitions
changed_at timestamptz Server time, not client time
note text Optional leader note at time of transition
location_lat decimal(9,6) Optional GPS at time of status change
location_lng decimal(9,6) Optional GPS at time of status change

2.5 New table: loyalty_pending_credits

Staging table used by the post-completion loyalty job to ensure idempotency.

Column Type Notes
id uuid PK
batch_id uuid FK → trek_batches
user_id uuid FK → users Trekker who completed the batch
points_to_credit integer Computed at job time from trek difficulty + loyalty rules
status credit_status (enum) pending | processing | credited | failed
job_run_id uuid BullMQ job ID — idempotency key
credited_at timestamptz Set when loyalty_points row is inserted
error_detail text Populated on failure for retry/manual review
💡
Unique constraint on (batch_id, user_id) — guarantees one credit row per trekker per batch regardless of retries.

3. Trek Batch Status Lifecycle

3.1 Status enum values

Status Meaning Who can set it Trigger
scheduled Batch is open for bookings; no trek activity yet System (default on create) Admin creates batch
assembling Bookings closed; participants are gathering at meeting point Trek Leader or Admin Leader taps 'Start assembly' — typically morning of Day 1
in_progress Trek is actively underway Trek Leader only Leader taps 'Begin trek' after headcount at assembly
paused Trek temporarily halted (weather window, medical situation, route issue) Trek Leader only Leader taps 'Pause trek'; must add a note explaining reason
completed Trek finished; all participants safely returned Trek Leader only Leader taps 'Complete trek'; triggers loyalty job
cancelled Batch will not run Admin only (not leader) Admin action; triggers refund workflow

3.2 Allowed transitions

From → → To Allowed? Notes
scheduled assembling Yes Leader or admin; closes new bookings
scheduled cancelled Yes Admin only
assembling in_progress Yes Leader only; sets started_at
assembling cancelled Yes Admin only (e.g. sudden weather)
in_progress paused Yes Leader only; note required
in_progress completed Yes Leader only; sets finished_at, triggers loyalty job
in_progress cancelled No Cannot cancel a running trek — use completed with incident note
paused in_progress Yes Leader only; resumes trek
paused completed Yes Leader only; if trek ends while paused (e.g. emergency extraction)
completed any No Terminal state — no further transitions
cancelled any No Terminal state — no further transitions

3.3 Status transition API

PATCH /api/v1/batches/:id/status

Request body: { to_status, note?, location? }

Authorization: caller must have batch_staff role = trek_leader for operational transitions (assembling → in_progress → paused → completed). Admin JWT can perform any transition.

Server-side logic on this endpoint:

  • Validate transition is in the allowed matrix above; return 422 if not
  • Check caller is active trek_leader on this batch (or admin)
  • Wrap in a transaction: UPDATE trek_batches SET status, status_updated_at, status_updated_by, started_at (if → in_progress), finished_at (if → completed)
  • INSERT into batch_status_history
  • If → completed: enqueue loyalty_credit BullMQ job with { batch_id, triggered_by, triggered_at }
  • If → in_progress or assembling: send push notification to all confirmed participants
  • Return updated batch object

4. API Routes — New & Updated

4.1 Batch staff management (Admin)

Method Path Description
GET /api/v1/admin/batches/:id/staff List all staff assigned to a batch with roles
POST /api/v1/admin/batches/:id/staff Assign a user to a batch with a role. Body: { user_id, role, notes? }
PATCH /api/v1/admin/batches/:id/staff/:staffId Update role or notes for a staff member
DELETE /api/v1/admin/batches/:id/staff/:staffId Remove (set is_active=false) a staff member from a batch
GET /api/v1/admin/users?role=trek_leader List users eligible to be assigned as leaders

4.2 Batch status management (Trek Leader / Admin)

Method Path Description
PATCH /api/v1/batches/:id/status Transition batch status. Body: { to_status, note?, location_lat?, location_lng? }
GET /api/v1/batches/:id/status-history Full audit log of all status transitions for a batch
PATCH /api/v1/batches/:id/incident Trek leader posts/updates incident note on a running batch
GET /api/v1/batches/live Public: list all batches currently in_progress or assembling (used for 'Ongoing Treks' feed)
GET /api/v1/batches/:id/live-status Real-time status card for a single batch (polling or SSE)

4.3 Leader-facing mobile routes

These routes are only visible/accessible to users who have an active batch_staff row with role = trek_leader.

Method Path Description
GET /api/v1/leader/batches Leader's assigned batches: upcoming, live, past
GET /api/v1/leader/batches/:id Full batch detail including participant list, docs status, staff roster
GET /api/v1/leader/batches/:id/participants All confirmed participants with doc/health status and emergency contacts
POST /api/v1/leader/batches/:id/notify Send push notification to all batch participants. Body: { title, message }

4.4 Guide-facing mobile routes

Method Path Description
GET /api/v1/guide/batches Guide's assigned batches
GET /api/v1/guide/batches/:id Batch detail — itinerary, participant count, emergency contacts (no payment data)
GET /api/v1/guide/batches/:id/participants Participant names and emergency contacts only

5. Frontend — Ongoing Treks Feature

5.1 Website: Ongoing Treks section

A new section on the Home page and a standalone /treks/ongoing page showing all batches currently in_progress or assembling.

Element Data source Behaviour
'Live now' badge batch.status = in_progress Pulsing green dot; visible on trek card in explore listing
'Assembling' badge batch.status = assembling Amber badge — 'Starting soon'
Ongoing Treks section (home) GET /api/v1/batches/live Horizontal scroll card strip; shows trek name, region, day N of M, leader name
Live status card GET /api/v1/batches/:id/live-status (poll 60s) Current status, day number, last status update time, incident note if any
/treks/ongoing page Same endpoint Full grid of all ongoing batches; filterable by region

5.2 Mobile app: Trek Leader screens

A new 'Leader HQ' section appears in the app for users with trek_leader role on any upcoming or active batch.

Screen Route Description
Leader home app/(tabs)/leader/index.tsx Cards for each assigned batch: upcoming / live / past
Batch control panel app/(tabs)/leader/batch/[id].tsx Status pill, big action button (context-aware), participant count, staff list
Status transition app/(tabs)/leader/batch/[id]/status.tsx Confirmation sheet with allowed next states, mandatory note input for pause/complete
Participant list app/(tabs)/leader/batch/[id]/participants.tsx Trekker names, doc status indicators, emergency contact tap-to-call
Notify participants app/(tabs)/leader/batch/[id]/notify.tsx Title + message compose; sends push to all confirmed participants
Status history app/(tabs)/leader/batch/[id]/history.tsx Timeline of all status changes with timestamps and notes
Incident log app/(tabs)/leader/batch/[id]/incident.tsx Free-text incident note; visible to admin, not to participants

5.3 Mobile app: Guide screens

Screen Route Description
Guide home app/(tabs)/guide/index.tsx Upcoming and active batch assignments
Batch view app/(tabs)/guide/batch/[id].tsx Read-only: itinerary, participant count, day-wise plan
Participant contacts app/(tabs)/guide/batch/[id]/contacts.tsx Names + emergency contacts (tap-to-call); no booking/payment data

5.4 Participant-facing: ongoing trek card

In the My Treks tab, a booking with an active batch shows an enhanced card:

  • Live status banner: 'Your trek is in progress — Day 3 of 7'
  • Last update timestamp from status_updated_at
  • Guide contact buttons (tap-to-call/WhatsApp for each guide)
  • Trek leader name and emergency contact
  • Incident note shown as a non-alarmist update banner if present (e.g. 'Short rest stop due to weather — continuing shortly')

6. Loyalty Credit on Trek Completion

6.1 Credit rules

Trek difficulty Base points awarded Notes
Easy 100 pts e.g. Kedarkantha meadows, beginner treks
Moderate 200 pts Most standard Himalayan treks
Difficult 350 pts Technical routes, high-altitude passes
Expedition 500 pts Summit attempts, multi-week treks

Multipliers applied on top of base points:

Multiplier condition Multiplier Example
First completed trek (new user) 1.5× Easy first trek: 100 × 1.5 = 150 pts
Repeat booking (2nd+ trek with OBS) 1.1× Moderate repeat: 200 × 1.1 = 220 pts
Trek completed in current season peak (configurable) 1.2× Peak season bonus
Full group (batch ≥ 90% full) 1.1× Rewarding popular batches
Multipliers stack multiplicatively Repeat + peak: 200 × 1.1 × 1.2 = 264 pts

6.2 BullMQ job: credit_loyalty_on_completion

Enqueued when batch status transitions to completed. Runs asynchronously — does not block the status update response.

Step Action Error handling
1 — Fetch participants Query all bookings WHERE batch_id = X AND status = 'confirmed' If no confirmed bookings: log and exit cleanly
2 — Check idempotency For each user, check loyalty_pending_credits for existing row with (batch_id, user_id) If status = credited: skip. If status = processing: skip (another job running). If status = failed: retry once then alert admin.
3 — Insert pending rows INSERT INTO loyalty_pending_credits (batch_id, user_id, points_to_credit, status='pending', job_run_id) ON CONFLICT DO NOTHING ON CONFLICT (batch_id, user_id) DO NOTHING — idempotency guarantee
4 — Compute points For each pending row: fetch trek.difficulty, check user's prior completed treks count, apply multipliers Round to nearest integer
5 — Credit points INSERT INTO loyalty_points (user_id, points_delta, reason='trek_completion', reference_id=batch_id) then UPDATE loyalty_pending_credits SET status='credited', credited_at=now() Wrap steps 5–6 in a transaction. If transaction fails: status stays 'pending' for retry.
6 — Update tier Recompute user's loyalty tier based on total points; UPDATE users.loyalty_tier if changed Run per-user after credit. Non-blocking — tier is cosmetic.
7 — Notify Send push notification per user: 'You earned X Bhaisahab Coins for completing [Trek Name]!' Notification failure does not roll back credits
8 — Award certificate Enqueue separate generate_certificate job per user Separate job — idempotent; certificate URL stored on booking

6.3 Points→tier recomputation

Tier is recomputed from the cumulative SUM of all positive loyalty_points rows for a user. The tier levels (from the loyalty_tiers table) are evaluated after every credit. A downgrade on tier is not applied automatically — only upgrades happen automatically (prevents punishing users for redemptions).

7. Admin Panel Additions

7.1 Batch staff assignment UI

  • On any batch detail page in admin, a 'Staff' tab shows current assignments
  • 'Assign staff' button opens a user search modal; select user → pick role → save
  • Enforce business rules in UI: warn if no trek_leader assigned; warn if fewer guides than recommended (trek.min_guides from trek config)
  • 'Replace leader' flow: assigns new leader, marks old one is_active=false, adds a status_history note

7.2 Live batch monitor

New /admin/live page showing all batches with status in_progress or assembling in a real-time table (polling 30s):

  • Batch name, trek, day N of M, leader name, participant count, last status update
  • Quick-expand to see incident notes and full status history
  • 'Contact leader' button (WhatsApp deep link or in-app message)
  • Admin can force-complete or cancel any running batch from here (with mandatory reason)

7.3 Loyalty audit dashboard

  • Table of all loyalty_pending_credits rows filterable by status
  • 'Failed' rows shown prominently with error_detail and a manual retry button
  • Batch completion summary: which batches have been credited, how many users, total points awarded

8. Migration Plan (Existing Data)

Step Action Notes
1 Run migration: add new columns to trek_batches (status, status_updated_at etc) All existing batches get status = 'scheduled' by default
2 Create new tables: batch_staff, batch_status_history, loyalty_pending_credits No data migration needed — tables start empty
3 Create enum types: trek_batch_status, batch_staff_role, credit_status Do this before altering tables that reference them
4 Migrate existing guide_id FK data For each existing batch with a guide_id, INSERT INTO batch_staff (batch_id, user_id=guide_id, role='guide', assigned_by=system_admin_id)
5 Drop guide_id column from trek_batches After verifying migration in step 4 is complete
6 Backfill status_history Optional: INSERT one 'scheduled' row per existing batch into batch_status_history for audit completeness
💡
Run all migration steps inside a single database transaction. Use a Prisma migration file with raw SQL for the enum creation (Prisma does not auto-generate enum creation in all cases).