How I Built Kridha
A B2B self-pickup marketplace for Tier-2 India — designed around real backend constraints, not a tutorial template.
Why Kridha Exists
Platforms like Udaan require 50 kg+ minimums to make delivery economics work. Kridha removes the minimum order constraint by eliminating delivery entirely — buyers self-pickup within 10 km. One constraint removed, a new market unlocked.
Read the case studyZero-Trust Auth System
HttpOnly cookie JWTs eliminate XSS token theft. Three-layer rate limiting (per-IP → per-account → global) defeats IP rotation. Token-family rotation detects refresh-token reuse and revokes all sessions in milliseconds.
Explore security designCorrectness by Construction
Order→SubOrder decomposition ensures one Razorpay advance covers all sellers atomically while fulfillment tracks stay independent. SELECT FOR UPDATE prevents stock oversell. Nineteen documented system invariants define what 'correct' means.
Read the architectureMeasured, Not Claimed
100 concurrent webhook deliveries → exactly 1 DB row (k6 verified). 200 concurrent reads on PostGIS radius search → 0 HTTP errors. pg.Pool configured at max=15 (prod) with explicit timeout and fail-open Redis.
View load test resultsEngineering Highlights
Production patterns applied deliberately — each decision documented, each claim verified.
JWT Auth + HttpOnly Cookies
Tokens stored in HttpOnly cookies — inaccessible to JavaScript, eliminating XSS-based session theft. CSRF mitigated via double-submit token pattern.
Token Family Rotation
Refresh token reuse triggers immediate family revocation — all sessions for the user are invalidated. Detects stolen tokens without requiring user action.
PostgreSQL Transactions
Multi-seller checkout uses a single atomic transaction: stock decrements, Order + SubOrder creation, and status history all commit or roll back together.
SELECT FOR UPDATE Row Locking
Concurrent checkout for the same product serialises through Postgres row-level locks. Exactly one buyer succeeds; others receive a clean 409. No application-level locks, no clock drift.
Razorpay Webhook Idempotency
WebhookLog table holds a @unique constraint on razorpayPaymentId. The handler runs inside a transaction — 100 concurrent identical deliveries produce exactly one database write, verified with k6.
Redis Rate Limiting (3 Layers)
Per-IP sliding window (5/min) → per-account using phone suffix (10/15 min) → global platform limiter (500/min). Defeats IP rotation at layer 2. Fail-open: Redis down never blocks legitimate requests.
GlitchTip Error Monitoring
Every 5xx captures a GlitchTip event via Sentry SDK. Token theft and credential-stuffing attempts trigger fatal-level alerts. Error rate visible in real time — not discovered from user complaints.
Pino Structured Logging
JSON logs with correlation IDs (requestId) on every request. Fourteen sensitive fields (PIN, OTP, bank account) are redacted automatically — logs cannot expose user credentials.
OTP Pickup Verification
Delivery OTP generated at payment capture and stored hashed on SubOrder. Seller verifies buyer-presented OTP to transition READY_FOR_OTP_VERIFICATION → COMPLETED. Invalid OTPs are rejected by the state machine.
Payment Reconciliation
Two-phase payment split: advance at order creation, remaining via payment link at pickup. Each phase tracked in Payment table with type and status. Payout cron transfers only on COMPLETED + PAID — never on PENDING or DISPUTED.
Architecture
Preview
Five distinct layers — each with a single responsibility. Hover each layer to see the key components it owns.
Security
Highlights
Eight independent security controls. Each card shows what's protected and what attack it defeats.
Explicit HS256 algorithm + expiry validation on every protected route via `jwt.verify()` with `algorithms: ['HS256']`.
Threat Prevented
Algorithm confusion attacks (e.g. RS256/HS256 swap). Unsigned or expired tokens accepted as valid.
Token family tracking. On reuse detection, all sessions for that user are immediately revoked.
Threat Prevented
Stolen refresh token used by attacker after legitimate user has already rotated. Silent session hijack.
`crypto.timingSafeEqual()` compares HMAC-SHA256 signature. Always returns 200 — invalid signatures are silently logged to prevent retry storms.
Threat Prevented
Spoofed Razorpay events confirming unpaid orders. Timing attacks leaking signature bytes via early-exit string comparison.
Delivery OTP stored hashed on SubOrder. State machine enforces READY_FOR_OTP → COMPLETED. Invalid OTPs rejected before DB write.
Threat Prevented
Buyer collecting goods without paying remaining amount. OTP brute-force bypassing pickup gate.
Per-IP (5/min) → per-account using phone suffix (10/15 min) → global platform (500/min). Fail-open on Redis failure.
Threat Prevented
Credential stuffing via IP rotation defeated at layer 2. Distributed brute-force capped at layer 3.
CSP with `frame-ancestors 'none'`, `object-src 'none'`, `base-uri 'self'`. HSTS preload (63 072 000s). X-Content-Type-Options, Referrer-Policy.
Threat Prevented
Clickjacking via iframe embedding. MIME-type sniffing. Protocol downgrade. Base-tag hijacking for open redirect.
JWTs stored in HttpOnly cookies — inaccessible to JavaScript. CSRF double-submit token validates every mutating request.
Threat Prevented
XSS scripts reading `localStorage` and exfiltrating JWT. CSRF forcing authenticated state-changing requests from attacker-controlled pages.
`safeString` Zod transform strips HTML from all user string inputs at the API boundary before reaching service layer.
Threat Prevented
Stored XSS via product name or seller store name rendered to other users. HTML injection in notification content.
All security controls follow OWASP Top 10 mitigation patterns. Sensitive fields (PIN, OTP, bank account) are redacted from Pino structured logs — credentials cannot appear in log storage. Error monitoring via GlitchTip alerts on token-theft and credential-stuffing patterns in real time.
Challenges solved.
Not CRUD.
Nine production-grade engineering problems — each with a real failure mode, a specific solution, and a measurable outcome. Click any card to expand the Problem → Solution breakdown.
Measured, not assumed.
All performance claims are backed by k6 load tests. Numbers below reflect local Docker with configured pg.Pool (max=50). Placeholder latencies are realistic targets after Redis cache is active.
@unique constraint + transaction. 100 concurrent identical payloads → exactly 1 write.
DB rows written
1 / 100
SELECT FOR UPDATE serialises concurrent checkouts. Excess buyers receive 409.
Oversells
0
PostGIS + Redis cache-aside. 200 concurrent VUs, zero server errors.
HTTP errors
0 / 4353
Per-account limiter defeats IP rotation. Global cap at 500 auth req/min.
Layer 2 fired
✓ confirmed
100 VUs, all 7 primary endpoints. Zero server errors across full test run.
HTTP 5xx
0 / 9702
Per-Endpoint Latency (ms) — Docker + Redis cache active
GET /api/health
GET /api/products (PostGIS + cache)
GET /api/products (PostGIS, cold)
POST /api/cart
POST /api/cart/checkout
POST /api/webhooks/razorpay
GET /api/orders
* Latency values are realistic targets. P95 on Supabase (cold) was 2300ms due to pg.Pool max=10 default — fixed by configuring explicit pool (max=15 prod / max=50 dev). Values above reflect post-fix Docker baseline.