A digital home for a college Provident Fund office. It takes the Trust's paper process — the pink part-final form, the green loan form, the salary sheet, the interest register — and turns it into a web portal and a phone app, without losing the thing that made the paper trustworthy: every rupee is accounted for, and every decision is signed by the person whose job it is to sign it.
This README is the friendly tour. Read it top to bottom and you'll know what the system is, how it thinks, how to run it, and where everything lives.
A provident fund is, at heart, a ledger — money in, money out, and what's left. So this system keeps a
real one: an append-only list of transactions (pf_transaction). Nothing stores "the balance" as a
number that could drift; the balance is always the sum of the ledger. Contributions, interest, a loan
being disbursed, a part-final being paid, a retirement being settled — each is a row, and every figure
on every screen is derived from those rows.
That single decision is why the numbers can be trusted. If a screen says you have ₹3,00,000, it's because the ledger adds up to ₹3,00,000, not because someone once typed 300000 into a field.
Three figures come up again and again, straight off the Trust's form:
| Figure | Means | How it's computed |
|---|---|---|
| Fund balance | What you've saved | contributions + VPF + interest − withdrawals |
| Loan outstanding | What you still owe | loans disbursed − principal repaid |
| Net credit | What you may draw against | fund balance − loan outstanding (never below 0) |
Every entitlement — how much you can borrow, how big a part-final you can take — is a percentage of net credit, never of the savings alone. Borrowing against the same rupee twice is simply not expressible.
PFMS/
├── Backend/ Spring Boot 3 · Java 21 · the domain, the ledger, the REST API
├── Frontend/ React 18 · TypeScript · Vite · Tailwind — the staff & subscriber web portal
├── pwa/ React · Vite — the installable subscriber mobile app (server-driven UI)
├── docs/ the paper forms, the meeting notes, the requirements, the workflow spec
├── docker-compose.yml run the whole stack in containers
├── PRD.md · STATUS.md · TESTING.md product spec, status, and the testing guide
└── README.md you are here
Three apps, one backend:
- Backend owns the truth. It exposes a REST API, enforces every rule, and posts to the ledger.
- Frontend is the full staff + subscriber portal: dashboards, the officer desks, finance & admin.
- pwa/ is a small, installable phone app for subscribers, whose screens are drawn by the server (see §7).
A part-final withdrawal is not one person's decision — it walks around the building and collects signatures. The system models that literally, with a login per desk, so the separation of duties is real and not a single rubber stamp.
| Role | Login (demo) | What they do |
|---|---|---|
| Subscriber (employee) | employee / employee123 |
View their PF, apply for a part-final or loan, track it |
| Establishment Section | establishment / establishment123 |
Verify an applicant's particulars |
| Internal Auditor | auditor / auditor123 |
Work out the ceiling; import the monthly salary sheet |
| Secretary | secretary / secretary123 |
Sanction, and record the cheque on disbursal |
| Committee (×3) | committee1..3 / committee123 |
Vote — every member must approve; the first denial rejects |
| Super Admin / Finance | superadmin / super123 |
Employees, rates, policies, interest runs, retirement settlement |
Sign in as different people to watch the same application from each side.
The heart of the system. An application moves through a state machine that will not let a stage be skipped — you cannot sanction something the employee never agreed to.
SUBMITTED → VERIFIED → ASSESSED → AWAITING_CONSENT → CONSENTED
→ UNDER_COMMITTEE → COMMITTEE_CLEARED → SANCTIONED → DISBURSED
From any live stage it can be REJECTED (a reason is mandatory) or WITHDRAWN by the employee. Two things make it more than a straight line:
- Consent. The Auditor routinely allows less than was asked for. The employee is sent the smaller figure to accept before anything moves — nobody has money taken out of their fund on a number they never saw.
- The committee. A multi-member body where any single member's denial ends the application.
The rules an applicant is held to (all shown before they apply, not discovered at the counter): the ceiling (a % of net credit), an application before the 25th of the month, a six-month gap between withdrawals, and an annual cap. Money only actually leaves the fund at disbursal, which is also the only moment the ledger is debited.
Loans are simpler — apply (up to 75% of net credit, choosing the number of EMIs), Finance approves, and a reducing-balance schedule is derived from the ledger. The interest rate is locked onto the loan at disbursal, so a later rate change never re-prices a loan already running.
You'll need JDK 21 and Node 18+. No database to install — local runs use in-memory H2.
Backend (port 8080):
cd Backend
./mvnw spring-boot:run -Dspring-boot.run.profiles=localThe local profile builds the schema from the Flyway migrations, then seeds a whole demo office (see
§6). Data lives in memory and is rebuilt fresh on every boot.
Frontend (port 5173):
cd Frontend
npm install
npm run devSubscriber PWA (port 5174):
cd pwa
npm install
npm run devOpen the frontend, sign in with any login from §3, and click around. The first time a screen loads,
a short guided tour introduces the portal (reopen it any time from the ? in the header).
Prefer containers?
docker-compose upbrings up the stack. The deployeddemoprofile runs on managed Postgres and reads every secret from the environment (see.env.example) — nothing sensitive is ever committed.
render.yaml is a blueprint for the whole stack. Open the button above (or Render → New →
Blueprint → pick this repo → Apply) and it provisions and wires together:
| Service | What it is |
|---|---|
pfms-db |
PostgreSQL |
pfms-backend |
the Spring app on the demo profile (Docker) |
pfms-web |
the staff + subscriber web portal |
pfms-app |
the subscriber mobile PWA |
Database credentials flow into the backend, the backend's URL is baked into both front-ends, and both front-end origins go into the backend's CORS list. The JWT signing key is generated by Render, so no secret is ever committed.
On its first boot the backend runs Flyway and then the seeders, so the site comes up with the full demo office already in it (~480 cases across every workflow). Seeding is idempotent, so restarts leave the data alone.
Free-tier notes: the first build takes roughly 5–10 minutes (Maven builds the image); free web
services sleep after inactivity, so the first request afterwards is slow; and a free Postgres expires
after 90 days. Any paid instance type removes all three caveats. If a service name is already taken
globally, rename it in render.yaml and update the two URLs that point at it.
A demo with one record per screen teaches you nothing about how it behaves under real volume, so the seeders fill the office the way a real one fills up.
- Base desks (
PfmsApplication.initData) — the officer and committee logins from §3. - 32 demo employees (
DemoDataSeeder) —firstname.lastname/employee123, with contribution history and loans in every status. - ~404 workflow cases (
WorkflowCaseSeeder) —case.emp.0001..case.emp.0404/case123, each resting at one interesting point of a workflow: part-finals at every chain stage, loans in every status, retirements pending / settled / declined. Every one is created through the real services, so it's in a state the state machine permits and any money it moved is on the ledger.
That's roughly 480 real cases across the workflows — enough to test every queue, every badge and every
edge. The per-bucket counts are constants in WorkflowCaseSeeder; dial them up for an even busier
office. Seeding is deterministic and idempotent, so a fresh boot reproduces the same office exactly.
A separate, self-contained guided tour (Frontend/.../TutorialCards.tsx) explains the portal to a
newcomer. It reads no seeded data — it describes the screens, it doesn't depend on what's in them — so
it stays isolated from the office dataset.
The pwa/ app is deliberately thin. It doesn't know what a "PF summary" looks like; it knows how to
draw a handful of primitives — a grid of figures, a section heading, a list, a card, a button — and it
draws whatever sequence of them the server sends for GET /api/app/screens/home.
The server builds that screen (SduiService) from the same services the web portal uses, so the
phone and the desktop can never show two different balances. And because the layout and the copy live
on the server next to the data, a screen can gain a figure or reword a hint without shipping a new
build of the app. The node vocabulary is defined once on each side — Backend/.../dto/sdui/ and
pwa/src/sdui.ts — and pwa/src/SduiRenderer.tsx is the single place that turns a node into UI.
The app is installable (web manifest + icon) and its shell opens offline (a minimal service worker),
though it never caches API responses — a stale balance is worse than none. See pwa/README.md for more.
Everything money-related funnels through a few well-named places, so a rule has one definition:
| Concern | Lives in |
|---|---|
| Posting money / reading balances | PfLedgerService, PfTransactionType |
| Turning a salary row into PF | ContributionService (derives from the scheme) |
| The part-final rules & eligibility verdict | PartFinalPolicy |
| The part-final chain | PartFinalService + PartFinalStatus (the state machine) |
| Loans & the repayment schedule | LoanService, LoanScheduleService |
| Effective-dated rates | RateService (throws rather than guessing a rate) |
| Editable policy values | PolicyService (unconfirmed guesses are surfaced to Finance) |
| Retirement settlement | RetirementService |
| Pre-filling the forms from the record | FormPrefillService |
| The mobile app's screens | SduiService |
The API is documented via OpenAPI — with the backend running, browse /swagger-ui.html.
The backend has a broad suite — unit tests for every service's rules, plus integration tests that boot the real app on H2 and exercise the migrations, security, and the seeders.
cd Backend
./mvnw testThe frontend and PWA typecheck and (for the frontend) run component tests:
cd Frontend && npm run typecheck && npm test
cd pwa && npm run typecheckTESTING.md has the fuller guide, including the end-to-end walkthrough of the part-final chain.
The docs/ folder is the paper trail this system replaces and the thinking behind it:
docs/media/— the actual Trust forms (the pink part-final, the green loan) that the software mirrors.docs/notes/2026-07-17-workflow-spec.md— every workflow, its depth and edge cases, and the audit's bug findings (all fixed).docs/notes/— the requirements meeting and the implementation-gap analysis.PRD.md— the product requirements.
The guiding rule when sources disagree: the printed forms win, then the meeting, then the written spec. The software is meant to match the office, not the other way round.
Welcome aboard. If you only remember one sentence, make it this one: the ledger is the truth, and every screen is just a view of it.