Skip to content

Onboarding

Two-step flow: profile + zone first, then KYC (gov ID + face). Both verifications must pass before profile save.


/register → /login → /onboarding → /dashboard
  1. Register (/register) - Email + password signup via Supabase Auth.

  2. Login (/login) - Email + password sign-in.

  3. 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.
  4. 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

The rider fills in:

FieldStored AsNotes
Delivery platformprofiles.platformzepto or blinkit
Full nameprofiles.full_nameAs on government ID
Phone numberprofiles.phone_numberFor UPI payout routing
Delivery zoneprofiles.zone_latitude, profiles.zone_longitudePinned on MapLibre map

Pressing Next advances to Step 2. Data is validated but not yet saved.


Both government ID and face verification are required before profile save.

  • Rider uploads an image of Aadhaar (or PAN, Voter ID, Driving License).
  • Image is uploaded to the government-ids storage bucket.
  • LLM vision model (OpenRouter) verifies document authenticity, legibility, and that it appears to be a valid government ID.
  • On success: government_id_url and government_id_verified are set on the profile.
  • 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:
    1. Gesture match - Is the requested gesture visible?
    2. Liveness - Does it appear to be a live person (not a photo/screen)?
  • Photos are stored in the face-photos bucket.
  • On success: face_photo_url and face_verified are set on the profile.

Optional GOV_ID_ENCRYPTION_KEY and FACE_PHOTO_ENCRYPTION_KEY can be set to encrypt stored images at rest.

Only when both verifications pass does the rider see the Continue button. On submit:

  1. Profile is upserted with all data.
  2. Premium recommendation is fetched from lib/ml/premium-calc.ts.
  3. Rider is redirected to /dashboard.

EndpointMethodDescription
/api/onboarding/verify-government-idPOSTUpload gov ID, run LLM verification, return status
/api/onboarding/verify-faceGETRequest random gesture
/api/onboarding/verify-facePOSTSubmit face photo, LLM checks gesture + liveness

See API Reference → Onboarding Endpoints for request/response shapes.


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):

lib/utils/geo.ts
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
}

After zone selection, the app calculates a recommended weekly premium:

lib/ml/premium-calc.ts
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”).


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;
}

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.ts
export 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`);
}

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.