MSME loan portal: draft/submit workflow, document vault, staff-scored credit rating
Problem
Small-business (MSME) credit assessment is done via unstructured paperwork; lenders need a structured intake, document collection, and a repeatable weighted scoring model to grade applicants.
What it does
A three-role Next.js app: MSMEs fill a 12-section application (onboarding, company profile, promoters, financials, banking, site visit, annexures) with document/video uploads and submit drafts; staff review submitted applications in a console and enter pillar scores; admins manage users, view analytics, logs, and a read-only 7-pillar scoring configuration. A seed script generates 100 synthetic MSME companies with applications and scores for portfolio analytics.
Architecture
[web] Next.js App Router UI (MSME dashboard, staff console, admin panel) (client) [middleware] Edge middleware (src/proxy.js) JWT gate + role routing (api) [api] Next.js Route Handlers (/api/auth, /api/application, /api/upload, /api/admin) (api) [auth] jose HS256 JWT session in httpOnly cookie (model) [store] data/db.json JSON file store (users, applications, activityLog, scoringConfig) (store) [files] uploads/ local filesystem for documents/videos (store) [seed] scripts/seed-companies.mjs synthetic data generator (worker) web ──▶ middleware · requests with sme_session cookie middleware ──▶ api · verified user injected as x-user-* headers api ──▶ store · synchronous readDB/writeDB per request api ──▶ files · multipart upload/download of vault files seed ──▶ store · seeds 100 demo applications + scores
Simplified — box-and-arrow sketch of the repo structure, not a system diagram.
Decisions
01
JSON file as database
MVP simplicity — whole state (users, applications, activity log, scoring config) read/written via fs to data/db.json on every request, no DB server
Evidence: src/lib/db.js
02
JWT middleware as single auth gate
src/proxy.js verifies the session cookie for all routes, enforces per-prefix role maps (/dashboard→msme, /console→staff, /admin→admin) and injects x-user-* headers; API routes additionally call requireAuth
Evidence: src/proxy.js
03
Normalizer layer over a 12-section schema
normalizeApplicationData() back-fills missing sections and aliases legacy key names (promoters_list, orgStructure) so drafts saved by older versions stay submit-valid
Evidence: src/lib/application-schema.js
04
Human-in-the-loop scoring engine
calculateScore() normalizes each pillar to 0–100, applies configurable weights, clamps inputs and maps to a grade scale — staff supply pillar scores manually for the MVP (scoring.js header comment says so)
Evidence: src/lib/scoring.js
05
Path-traversal-safe file vault with role-based visibility
uploads GET resolves path.basename, checks the file belongs to the application record, hides drafts from staff, and scopes msme users to their own files
Evidence: src/app/api/upload/route.js
06
Deterministic seed data for analytics demo
seed-companies.mjs uses a seeded mulberry32 PRNG to generate 100 idempotent demo companies, computes pillar scores from synthesised financials, and reuses the same weighted formula as scoring.js
Evidence: scripts/seed-companies.mjs
The hard parts
- Data integrity of a whole-JSON-file store under concurrent writes (read-modify-write with no locking in src/lib/db.js)
- Keeping the large multi-section draft schema backward-compatible across edits (application-schema.js alias handling)
- Upload authorization matrix: staff/admin can see submitted-but-not-draft files, msme only their own (upload/route.js)
Known limitations
- Plaintext password comparison in login route, no hashing (src/app/api/auth/login/route.js)
- Hardcoded fallback JWT secret in source and cookie secure:false explicitly marked 'MVP — no HTTPS' (src/lib/auth.js, src/proxy.js)
- src/app/api/admin/applications/[id]/update/route.js is an empty 0-byte file — admin status-update endpoint is unimplemented
- No database, no migrations, no tests, no Dockerfile/CI
- data/db.json with demo credentials is committed to the repo
- Scoring inputs are manually entered by staff, not derived from documents/financials