noise -> signal

000

press any key to skip

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

  1. 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

  2. 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

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

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

  5. 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

  6. 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

Command palette

Jump to a section, project or link