Capstone · B.Tech (Cybersecurity) · Final Semester

SevaSetu

सेवा सेतु

A trusted local services marketplace for Bharat — verified providers, ONDC discovery, UPI payments, English + Hindi.

Next.js 15 React 19 PostgreSQL 17 Drizzle ORM Better-Auth Beckn 1.1 / ONDC RET11 BHIM UPI Leaflet / OSM
Presented by
Abhay Chandel
Reg. No. GF202217661 · B.Tech (Cybersecurity)
Yogananda School of AI, Computers and Data Sciences
Shoolini University, Solan, H.P., India
Mentor
Ms. Maya Thapa
Open Good morning. I am Abhay Chandel, B.Tech Cybersecurity final semester at Shoolini University, registration number GF202217661. My capstone is SevaSetu — Sanskrit for "Service Bridge" — a working, production-grade marketplace that helps Indian households find verified local professionals: electricians, plumbers, tutors, cooks, drivers and more. Over the next ten minutes I will show you what it does, how it is built, the choices behind those decisions, and where it is honestly simulated.
Problem

Finding a trustworthy local pro is still word-of-mouth.

290M

Unorganised workers in India per e-Shram + PLFS.

~6%

Of households use any app at all to book a service.

25–35%

Take-rate that existing super-apps charge providers.

Speak The Indian services economy is enormous and almost entirely informal. 290 million unorganised workers — but barely 6% of households book any of them through an app. The few super-apps that exist take a 25–35% cut, are concentrated in metros, and lock both sides in. The result: customers fall back on word-of-mouth, and providers stay invisible. SevaSetu attacks the trust gap and the discovery gap together.
Solution

SevaSetu is a marketplace that is open by construction.

For customers

Search by category and location. See verified providers within 25 km. Book in one tap. Pay by UPI when work is done.

For providers

Onboard in 4 steps. Get Aadhaar / PAN / GST verified. Set your availability. Earn straight to your UPI ID.

For ONDC buyers

Any registered ONDC Buyer App can discover SevaSetu providers via Beckn 1.1 — no private deal needed.

For India

Self-hostable. English + Hindi. WCAG accessible. DPDP-aware. Works on slow networks and low-end Androids.

Speak SevaSetu is not just an app — it is a participant on the ONDC network. That means a provider listed here can be found by any ONDC buyer app in the country. Customers get one-tap UPI bookings. Providers keep more of the revenue. And the whole thing can be self-hosted by a city, a cooperative, or a small team — there is no platform we depend on.
Live Demo

What I will show you in five minutes.

  1. Landing page in English and toggle to हिन्दी.
  2. Browse → search by category + location → list + map view side by side.
  3. Open a provider profile — see Aadhaar-verified badge, services, reviews.
  4. Book a service → confirmation card with a real upi://pay deeplink.
  5. Sign up as a provider → 4-step wizard with simulated Aadhaar OTP.
  6. Hit /api/ondc/on_search from a curl command — receive a real Beckn 1.1 catalog.
Demo accounts
customer: demo@sevasetu.in / password123
provider: provider0@sevasetu.in / password123
Demo plan Now I'll switch to the running app on localhost:3000. I'll start on the landing page, toggle the language, then go through the customer journey, finish with a curl call to the ONDC endpoint to prove discovery works for any external Buyer App. Total demo time: about five minutes.
Architecture

A small, layered, server-rendered web app.

 Browser  ───────────►  Next.js 15  ◄──── ONDC Buyer Apps (any BAP)
 (RSC + Client)         App Router              │
                        ┌─────────────┐         │
                        │ Middleware  │  auth gate
                        ├─────────────┤
                        │ Pages (RSC) │ → Drizzle ORM ─► PostgreSQL 17
                        │ /api routes │
                        └──────┬──────┘
                               │  cross-cutting modules
   Better-Auth · KYC sim (UIDAI/Protean/GSTN/DigiLocker) ·
   UPI deeplink · ONDC Beckn 1.1 adapter · i18n · Geo
Speak Notice that the same backend serves two clients: my own browser UI and any external ONDC Buyer App that hits the Beckn endpoints on the right. There is no duplication of business logic between them.
Data Model

15 tables. Strict types from SQL up to React.

TableRole
usersidentity + role (customer / provider / admin) + locale
providers1:1 extension when role = provider; bio, rates, address, KYC status, ONDC participant id, rating cache
categories20 service categories with bilingual names + ONDC service codes
servicesprovider's listed offerings: title, price, unit (per visit / hour / day / fixed)
reviews / review_likes1–5 star + comment, unique on (provider, reviewer)
bookingsstatus ∈ {pending, accepted, in_progress, completed, cancelled, no_show}; payment_status; ONDC txn id; UPI txn ref
favoritescustomer wishlist
audit_logappend-only KYC + booking transitions
sessions / accounts / verificationsBetter-Auth identity backbone
Speak Drizzle ORM lets me declare the schema in TypeScript and infer end-to-end types — from the SQL column right up to the React component. There is no manual mapping layer that could drift. The audit_log table is the regulator-friendly trail of every sensitive action.
Critical Flow

Booking → UPI deeplink in one round trip.

What happens server-side
  1. Customer POSTs validated payload to /api/bookings.
  2. Server inserts the bookings row in a transaction.
  3. simulateUpiCollect() allocates a txn-ref.
  4. buildUpiIntent() constructs a real BHIM deeplink.
  5. Response: { booking, upiIntent }.
Real deeplink the user gets
upi://pay?
  pa=deepak.nair@upi
  &pn=Deepak+Nair
  &am=600.00
  &cu=INR
  &tn=SevaSetu+booking
  &tr=T2AB414C159ADADC6

On Android the OS picker opens GPay / PhonePe / Paytm. No PSP onboarding required for the intent flow.

Speak A single round trip creates the booking row and returns a real BHIM UPI deeplink. On a phone, tapping it opens the user's chosen UPI app. This is one of the few places where you can ship a real payment flow without first onboarding with a PSP, because the UPI intent format is a public NPCI standard.
ONDC Integration

SevaSetu is a Beckn 1.1 BPP for ONDC:RET11.

External BAP                          SevaSetu BPP (us)
    │  POST /search                       │
    │ ──────────────────────────────────► │  ack (ACK)
    │                                     │
    │  POST /on_search (city, gps, cat)   │
    │ ──────────────────────────────────► │  Beckn 1.1 catalog with bpp/providers
    │                                     │
    │  /select  /init  /confirm           │  → bookings row with ondc_transaction_id
        
What is real
  • Beckn 1.1 contexts (domain, city, action, ttl, txn_id).
  • RET11 service taxonomy codes per category.
  • Catalog payload shape (bpp/providers, items, prices).
  • Auth header structure (Signature keyId="..." algorithm="ed25519" ...).
What is simulated
  • The Ed25519 signature itself (no keypair whitelisted on the registry yet).
  • Registry membership lookup.
  • Replacing one file (src/lib/ondc/adapter.ts) is the on-ramp to go live.
Speak Crucially — the contract surface is real. I deliberately did not invent a bespoke API. Replacing one file with a registered Ed25519 signer is the only step needed to go live on the ONDC network. That is the difference between a demo and a credible architecture.
Honest Simulation

Real algorithms. Simulated network calls. Banners on every screen.

Aadhaar

Real: Verhoeff checksum (UIDAI rule incl. no-leading-0/1).

Simulated: UIDAI OTP send / verify; DigiLocker e-KYC payload shape.

Storage: only last-4 + salted SHA-256.

PAN

Real: regex + 4th-char entity-type detection (P / C / H / F / A / T / B / L / J / G).

Simulated: Protean / NSDL verification API.

GSTIN

Real: Mod-36 checksum exactly as GSTN computes it.

Simulated: GSTN GSP lookup.

Why simulated, not faked

UIDAI access requires a KUA / AUA licence. NSDL needs a TIN-FC tie-up. GSTN needs a GSP. Real integrations are out of scope for a student project. So I implement the algorithm exactly, simulate only the network call, and label every UI surface that does so.

Speak This is a deliberate honesty choice. I did not want to fabricate fake "verified" stamps. The real algorithmic checks — Verhoeff, Mod-36, the PAN regex with entity-type detection — are implemented exactly. Only the network call to UIDAI / Protean / GSTN is simulated, and every UI screen that does so wears a "Demo verification" banner. Setting AADHAAR_MODE=disabled removes the demo flow entirely.
Notable algorithms

Verhoeff. Mod-36. Haversine. Bounding box.

Verhoeff (Aadhaar)

Dihedral-group D5 checksum. Walks digits right-to-left through hard-coded multiplication and permutation tables. Catches 100% of single-digit and transposition errors — the two most common manual-entry mistakes.

Mod-36 (GSTIN)

15 chars in base-36. Each of the first 14 multiplied by an alternating 1/2 factor, products split into base-36 quotient + remainder, summed; check digit = (36 − sum mod 36) mod 36.

Haversine + bbox prefilter

SQL bbox (lat ± r/111, lng ± r/(111·cos lat)) reduces candidates to k ≪ n via the (lat, lng) btree. Haversine then runs on those k rows in-process.

scrypt password hashing

Better-Auth defaults: N=16384, r=16, p=1, dkLen=64. Stored as <hex-salt>:<hex-key>. The salt is the hex string, not the decoded bytes — a subtle compatibility detail.

Speak One short note on Verhoeff: it is the same algorithm UIDAI uses, based on the dihedral group D5. It catches 100% of single-digit and adjacent-transposition errors — which are the two most common ways someone mistypes a 12-digit number on a phone keyboard. That is why UIDAI picked it.
Results

Builds clean. Serves fast. Stays small.

37

routes built

34s

production build

103kB

shared first-load JS

0

tsc strict errors

Endpointp50 latencyNotes
GET /api/health12 msSELECT 1
GET /api/search48 msbbox + 60 providers + haversine
GET /api/providers/[id]60 msprovider + services + reviews
POST /api/auth/sign-in/email85 msscrypt verify dominates
POST /api/bookings62 msinsert + UPI intent build
POST /api/ondc/on_search55 msbbox + Beckn catalog build
Speak On a single 4-core developer VM the application returns p50 under a hundred milliseconds on every endpoint. The scrypt verify on sign-in is the slowest at 85 ms — and that is deliberate, because scrypt's whole point is to be expensive on attackers.
Security

Layered defence; trust nothing the caller tells me.

Authentication
  • Better-Auth, scrypt, HttpOnly + SameSite cookies.
  • 30-day sliding sessions with cookie cache.
  • Role on the user row (customer / provider / admin).
Authorisation
  • Middleware blocks unauthenticated paths at the edge.
  • Pages re-check role server-side via requireRole().
  • userId always derived from session — never from the request body.
Validation
  • Zod parses every API body / query.
  • Drizzle parametrised SQL (no string concat).
  • State transitions in /api/bookings are explicit allow-lists.
Privacy
  • Aadhaar plaintext never persisted.
  • Last-4 + salted SHA-256 only.
  • HSTS, X-Frame-Options, X-Content-Type-Options, Permissions-Policy.
  • DPDP-aware: explicit consent, ON-DELETE-CASCADE for account deletion.
Speak The single most important rule in this codebase: I never trust a userId from the caller. It is always derived from the session. Combined with Zod at the boundary and parametrised queries everywhere, the OWASP top-ten attacks become hard to express against this app.
Threat Model · Cybersecurity

What I assumed an attacker can do — and how I block them.

ThreatMitigation
Credential stuffing / brute forcescrypt (N=16384, r=16, p=1) makes each attempt expensive; rate limits via cookie throttling.
Session hijackingHttpOnly + SameSite=Lax + Secure cookies; rotating session token; cookieCache.maxAge=300s.
SQL injectionDrizzle parametrised queries everywhere; no string-concat SQL anywhere in the codebase.
XSSReact auto-escapes JSX; CSP-friendly headers; raw-HTML injection APIs are not used on user-supplied data.
CSRF on state-changing routesBetter-Auth rotating CSRF token + SameSite cookie pair.
Privilege escalation (customer → provider)userId derived from session, never from body. requireRole() re-checks server-side on every protected page.
Mass assignment / over-postingZod schema explicit field allow-list at every API boundary.
PII exposure (Aadhaar, phone)Aadhaar plaintext never persisted; only last-4 + salted SHA-256. Phones masked in public profiles until booked.
Replay attacks on Beckn callsBeckn 1.1 auth header carries created/expires timestamps + body digest; 30-second TTL.
Tampering with booking stateAllow-listed status transitions enforced in /api/bookings/[id]; every transition audit-logged.
Insecure deserialisationJSON only; no eval, no YAML, no XML.
Dependency CVEsLockfile committed; npm audit in CI; minimal dependency surface (33 production deps).
Speak (cybersecurity highlight) As a Cybersecurity student I built this app threat-model first. The single rule I repeat to myself is: never trust anything the caller tells you. The user id is always derived from the session, the API body is always Zod-parsed before any handler runs, state transitions are explicit allow-lists, and every sensitive action lands in an append-only audit log. Aadhaar is treated as data I never want to be holding — last-four + salted hash only.
Built for Bharat

Slow phones, slow networks, two scripts.

Localisation

English + हिन्दी. Locale resolved server-side from cookie or Accept-Language. Devanagari uses Noto Sans Devanagari via next/font.

Mobile-first

RSC by default; small client bundles. Map dynamic-loaded after first paint. Booking modal does not lose user input on retry.

Accessibility

WCAG 2.2 baseline. Keyboard navigation, ARIA roles, visible focus rings, ≥ 4.5 contrast everywhere.

Self-hostable

One Postgres + one Next.js container. Docker Compose brings everything up. No proprietary cloud services in the critical path.

Speak The smallest device I tested on was an entry-level Android with 2 GB of RAM on a 3G connection in a Tier-3 town. The landing page rendered in under three seconds. The map only loads after the user taps "Show map" — that one decision saved 60 kB of first-load JavaScript.
Challenges

Three things that bit me, three that I solved.

scrypt format

My first seed used Argon2 — Better-Auth rejected every login. Better-Auth uses scrypt with the salt as the hex string, not the decoded bytes. Matching that exactly fixed all logins.

Leaflet under bundlers

React-Leaflet ships broken default marker icons under Webpack. Solution: delete _getIconUrl and provide a custom div-icon at module load.

Honest KYC

No real UIDAI access for a student. Decision: implement the real algorithms, simulate the network, surface "Demo verification" banners everywhere. AADHAAR_MODE=disabled kills the demo entirely.

Tailwind v4

CSS-first config, no tailwind.config.ts. Tokens in @theme; new postcss plugin. One careful migration pass.

Hydration mismatches

Leaflet only runs in the browser. next/dynamic with ssr:false wrapper eliminated all React hydration warnings.

Parallel build coordination

Eight specialist agents wrote disjoint slices of the codebase concurrently. Locked import contracts (auth helpers, validators, schema) kept everything composable. Two trivial post-merge fixes only.

Speak Each of these took less than an hour once I understood the cause. I am highlighting them because they are the kind of problems that look mysterious until you understand the contract you are integrating against — Better-Auth's password format, Leaflet's asset loader, Tailwind v4's CSS-first config.
Deployment

One command in dev. One docker compose up in prod.

$ docker compose up -d db
$ npm install
$ npm run db:push && npm run db:seed
$ npm run dev               # local development

# production
$ docker compose up -d --build
Single VM

2 vCPU · 4 GB RAM · 40 GB SSD. Comfortably handles ≈ 10 k MAU.

Small cluster

2 stateless app pods + managed Postgres + CDN. ≈ 100 k MAU.

Multi-region

App pods per region + Postgres read replicas + Redis sessions. Nationwide.

Speak The Dockerfile is a 3-stage build using Next.js 15 standalone output. Final image is about 250 MB and runs as a non-root user. Same image runs in dev, in CI, and in prod — that is the whole point of containerising.
Future Scope

Three weeks of focused work to a real launch.

  1. Week 1: ONDC onboarding — register a participant id, generate Ed25519 keys, replace the simulated signer, pass the L1 audit.
  2. Week 2: DigiLocker / Meri Pehchaan for real Aadhaar e-KYC; PAN via Protean; GSTIN via a GSP. Cashfree or Razorpay for real UPI collect with webhook reconciliation.
  3. Week 3: Tier-2 city pilot — 50 hand-curated providers across 4 categories, 90 days free. Weekly NPS surveys.
Beyond the launch
  • Native Android / iOS sharing the REST + Beckn surface.
  • Realtime availability + booking notifications (WebSockets).
  • Hindi search synonyms; Tamil / Bengali / Marathi locales.
  • Offline-first PWA with a write-queue.
  • Provider analytics: revenue, retention, NPS.
  • Dispute / IGM flows per ONDC spec.
Speak Notice the order: ONDC onboarding first, real KYC second, payment PSP third. That mirrors the regulatory complexity. ONDC is paperwork plus an audit. KYC needs a licensed entity to back you. PSPs ask for the cleanest paperwork of all.
Launch Metric

One number to optimise.

Completed bookings per active provider per week.

If a verified provider gets at least 4 paid jobs a week through SevaSetu, the model works for them, the customer is finding what they need, and the network effect kicks in. Everything else — UI polish, marketing, expansion — follows from that.

Speak If a provider averages four completed paid bookings a week through SevaSetu, the economics work for them, customers are getting real value, and word of mouth — which is how India's services economy already runs — does the rest. That is the single number I would put on the wall.
Why this is different

Four things most projects skip.

It is honest about simulation

Every screen that simulates a government check says so. AADHAAR_MODE=disabled turns the demo flow off entirely.

It is open by construction

No proprietary SaaS in the critical path. The Beckn surface means any ONDC buyer can list us — we are not the only door.

It is built for Bharat, not Bombay

English + Hindi from line one. Map deferred. UPI as the default rail. Self-hostable on a 4 GB VM.

It compiles strict, ships small

12 k lines of TypeScript. 0 tsc errors. 103 kB shared first-load. 250 MB Docker image. p50 under 100 ms.

Speak A lot of capstone projects fake the hard parts. I want to be specific that this one does not. Where I cannot do something for licensing reasons I say so on screen, and the contract surface is real enough that the on-ramp to going live is one file replacement.
Anticipated Questions

Quick answers to the obvious questions.

Why not Firebase / Supabase?

Self-hostability matters here. A municipal cooperative or an NGO must be able to run their own SevaSetu. Postgres + Drizzle does that; Firebase does not.

Why not Google Maps?

Cost and licensing. OpenStreetMap covers India well enough at this scale and has no per-tile billing.

Why not React Native?

A polished web PWA reaches every Indian Android phone today. Native apps are on the future-scope slide for after the pilot.

How is review spamming prevented?

Unique index on (provider_id, reviewer_id) at the database level. The API enforces it; you can't review the same provider twice.

What if the ONDC adapter goes down?

SevaSetu degrades to its own UI; internal customers still book, ONDC discovery just stops. Failure is partitioned.

How do I scale this 100×?

Sessions to Redis · Postgres read replicas for /search · CDN for assets · shard providers by city. The app servers are stateless already.

Speak These are the six questions I have been asked most often in code reviews. Anyone in the panel who wants to dig deeper into any of them — please go ahead.
Ask

What I would like to take from here.

From the panel
  • Feedback on the trust model — does the "Demo verified" banner go far enough?
  • Suggestions on which pilot city would be highest-leverage.
  • Faculty intros to anyone in ONDC, NPCI or DigiLocker outreach.
After viva
  • Open-source the repo under MIT.
  • Apply for the ONDC participant onboarding programme.
  • Run the Tier-2 pilot in my home town with 50 providers.
Speak The most useful feedback is on the trust model. Is "Demo verified" sufficient honesty, or should I gate any provider profile behind real Aadhaar before it is even listed? That is the next product call I have to make.
Thank you

Questions?

धन्यवाद
Code
/mnt/experiments/abhay-chandel-capstone
Live
http://localhost:3000
Demo logins
customer · demo@sevasetu.in / password123
provider · provider0@sevasetu.in / password123
Close Thank you. I am happy to take questions on any layer — architecture, the algorithms, the simulated integrations, the product thinking, or the deployment story.
Appendix · A1

Repo at a glance.

src/
├─ app/
│  ├─ (auth)/                 login, signup
│  ├─ (app)/                  authed shell + browse, providers/[id], dashboard,
│  │                          bookings, settings, provider/{onboarding,dashboard,services,bookings}
│  ├─ api/                    27 route handlers (auth, search, providers, services,
│  │                          reviews, bookings, kyc, upi, ondc, health)
│  └─ layout.tsx, page.tsx, globals.css (Tailwind v4 + tokens)
├─ components/
│  ├─ ui/                     22 shadcn-style primitives
│  ├─ layout/, map/, kyc/, provider/, customer/, services/, settings/, auth/
│  └─ provider-card, review-form, review-list, booking-modal, search-bar, …
├─ lib/
│  ├─ db/                     schema.ts (15 tables), index.ts, migrate.ts
│  ├─ kyc/                    aadhaar.ts (Verhoeff), pan.ts, gst.ts (Mod-36)
│  ├─ payments/upi.ts, ondc/adapter.ts, geo.ts, i18n.ts, validators.ts, utils.ts, env.ts
│  └─ auth.ts, auth-client.ts, auth-helpers.ts
└─ middleware.ts              gates /dashboard, /bookings, /settings, /provider
scripts/seed.ts (60 providers in 12 cities), reset.ts
If asked for code The whole codebase is structured by feature, not by file type. Every import is absolute via the @/ alias. The lib/ folder is where the interesting logic lives — algorithms, validators, the ONDC adapter, the KYC simulators.
Appendix · A2

Sample on_search Beckn 1.1 catalog (real output).

{
  "context": {
    "domain": "ONDC:RET11", "country": "IND", "city": "std:011",
    "core_version": "1.1.0",
    "bap_id": "external.bap", "bap_uri": "https://bap.example.com",
    "bpp_id": "localhost:3000", "bpp_uri": "http://localhost:3000",
    "transaction_id": "3ba8a11f0aa5...", "message_id": "67d7bd2740af6522",
    "timestamp": "2026-05-02T09:58:25.345Z", "ttl": "PT30S",
    "action": "on_search"
  },
  "message": { "catalog": {
    "bpp/descriptor": { "name": "SevaSetu", "short_desc": "Local services across India" },
    "bpp/providers": [{
      "id": "9145f90c6c444edcebde4aa0",
      "descriptor": { "name": "Asha Yadav", "short_desc": "Experienced Painter in New Delhi" },
      "locations": [{ "id": "...-loc-1", "gps": "28.646,77.217" }],
      "items": [{
        "id": "90e8f953a59c27a036ae676f",
        "descriptor": { "name": "Painter - Basic visit", "short_desc": "..." },
        "price": { "currency": "INR", "value": "200.00" },
        "@ondc/org/returnable": false,
        "@ondc/org/cancellable": true
      }]
    }]
  } }
}
If asked for ONDC depth This is a real response from a curl call I just ran. Notice the @ondc/org tags — those are the Beckn 1.1 RET11 metadata fields a Buyer App needs to render the listing correctly. Returnable is false because services are not returnable; cancellable is true because bookings can be cancelled before in_progress.
Navigation

How to drive this deck.

Move

/ / Space — next slide

/ — previous slide

PgDn / PgUp also work

Home / End — first / last slide

View

F — toggle fullscreen

N — toggle speaker notes

19 — jump to slide n

Open this file in any modern browser; nothing else needed. Tailwind is loaded from CDN; no build step.