Onboarding
Two-step flow: profile + zone first, then KYC (gov ID + face). Both verifications must pass before profile save.
Flow Overview
Section titled “Flow Overview”/register → /login → /onboarding → /dashboard-
Register (
/register) - Email + password signup via Supabase Auth. -
Login (
/login) - Email + password sign-in. -
Onboarding (
/onboarding) - Two-step form:- Step 1: Platform, full name, phone, delivery zone.
- Step 2: Government ID upload + face liveness verification. Both must pass before profile save.
-
Dashboard (
/dashboard) - Rider’s home screen.
The root / page checks for an active session and active policy:
- Authenticated + has active policy →
/dashboard - Authenticated + no policy →
/onboarding - Unauthenticated →
/login
Step 1: Basic Profile
Section titled “Step 1: Basic Profile”The rider fills in:
| Field | Stored As | Notes |
|---|---|---|
| Delivery platform | profiles.platform | zepto or blinkit |
| Full name | profiles.full_name | As on government ID |
| Phone number | profiles.phone_number | For UPI payout routing |
| Delivery zone | profiles.zone_latitude, profiles.zone_longitude | Pinned on MapLibre map |
Pressing Next advances to Step 2. Data is validated but not yet saved.
Step 2: KYC Verification
Section titled “Step 2: KYC Verification”Both government ID and face verification are required before profile save.
Government ID Verification
Section titled “Government ID Verification”- Rider uploads an image of Aadhaar (or PAN, Voter ID, Driving License).
- Image is uploaded to the
government-idsstorage bucket. - LLM vision model (OpenRouter) verifies document authenticity, legibility, and that it appears to be a valid government ID.
- On success:
government_id_urlandgovernment_id_verifiedare set on the profile.
Face Liveness Verification
Section titled “Face Liveness Verification”- Rider is shown a random gesture (e.g. “Close your left eye”, “Smile”, “Look up”).
- Webcam captures a photo.
- Photo is checked by an LLM vision model for:
- Gesture match - Is the requested gesture visible?
- Liveness - Does it appear to be a live person (not a photo/screen)?
- Photos are stored in the
face-photosbucket. - On success:
face_photo_urlandface_verifiedare set on the profile.
Optional GOV_ID_ENCRYPTION_KEY and FACE_PHOTO_ENCRYPTION_KEY can be set to encrypt stored images at rest.
Continue Flow
Section titled “Continue Flow”Only when both verifications pass does the rider see the Continue button. On submit:
- Profile is upserted with all data.
- Premium recommendation is fetched from
lib/ml/premium-calc.ts. - Rider is redirected to
/dashboard.
API Endpoints
Section titled “API Endpoints”| Endpoint | Method | Description |
|---|---|---|
/api/onboarding/verify-government-id | POST | Upload gov ID, run LLM verification, return status |
/api/onboarding/verify-face | GET | Request random gesture |
/api/onboarding/verify-face | POST | Submit face photo, LLM checks gesture + liveness |
See API Reference → Onboarding Endpoints for request/response shapes.
Zone Selection
Section titled “Zone Selection”The ZoneMap component renders an interactive MapLibre map using OpenStreetMap tiles. The rider can drag a marker to their primary delivery area. Coordinates are reverse-geocoded via the Nominatim API (no API key, 1 req/s policy):
export async function reverseGeocode(lat: number, lng: number): Promise<string | null> { const res = await fetch( `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=14`, { next: { revalidate: 3600 } } // Next.js cache for 1h ); // Returns "Koramangala, Bengaluru" style string}Premium Recommendation
Section titled “Premium Recommendation”After zone selection, the app calculates a recommended weekly premium:
const historicalCount = await getHistoricalEventCount(supabase, lat, lng);const forecastFactor = await getForecastRiskFactor(supabase, lat, lng);const premium = calculateWeeklyPremium({ historicalEventCount, forecastRiskFactor });The result is shown to the rider before they choose a plan, along with a risk explanation (“Your zone has seen N disruptions in the past 4 weeks”).
Profile Data Model
Section titled “Profile Data Model”interface Profile { id: string; // matches auth.users.id full_name: string | null; phone_number: string | null; platform: 'zepto' | 'blinkit' | null; zone_latitude: number | null; zone_longitude: number | null; primary_zone_geofence: object | null; government_id_url: string | null; government_id_verified: boolean | null; face_photo_url: string | null; face_verified: boolean | null; role: 'rider' | 'admin'; created_at: string; updated_at: string;}Auth Callback
Section titled “Auth Callback”The Supabase OAuth callback route lives at /auth/callback (inside the (auth) route group). It exchanges the authorization code for a session and redirects to /dashboard (or the next query param):
// app/(auth)/auth/callback/route.tsexport async function GET(request: Request) { const code = searchParams.get("code"); if (code) { await supabase.auth.exchangeCodeForSession(code); return NextResponse.redirect(`${origin}${next}`); } return NextResponse.redirect(`${origin}/login?error=auth_callback_error`);}Admin Onboarding
Section titled “Admin Onboarding”Admins are regular riders whose email is in ADMIN_EMAILS or whose profiles.role is 'admin'. An existing admin can promote another user via Admin → Riders → [Rider] → Update Role.