fullstack
Kopsp
Savings and loan cooperative platform where money only moves through verified approvals, with an audit trail behind every change.
- Role
- Fullstack
- Date
- January 2026
Technologies
- go
- gin
- nuxt
- vue
- typescript
- postgresql
- drizzle
- better-auth
- zod
- unocss
- jose
Problem
A cooperative handles other people’s money, and the hard part is not the feature list, it is correctness. Two staff members approving the same deposit. A member paying their “Pokok” saving twice. A loan disbursed on a double-click. And because nothing here goes through a payment gateway, every rupiah arrives as a manual transfer that has to be tied to a proof uploaded by a member and to a staff member willing to vouch for it.
Solution
Three layers with rules about what each may do. The browser talks to Nuxt, Nuxt talks to a Go API,
and only Go writes the domain tables. Drizzle is scoped to the auth tables alone; the BFF never
queries a savings or loan table. The browser never sees the Go token either: it holds a session
cookie, and the Nitro server mints a short-lived HS256 JWT to forward. Identity fields (who
verified, who approved, which member) are injected from that session and ignored in the request
body, so a client cannot claim to be someone else. Members see only their own rows, enforced by a
WHERE member_id scope rather than by the UI hiding a link.
Where money is involved the checks are deliberately boring. Balances change only on approval, in
one atomic transaction. Status transitions are compare-and-set against the current status, so a
retried request returns a conflict instead of applying twice. The rule that a member can pay their
“Pokok” saving only once is a partial unique index, not an if in a handler: it holds even if
someone later writes a new endpoint and forgets. And the audit log is written inside the same
transaction as the action: if the log cannot be written, the action is rolled back. A ledger that
can be changed without a trace is not a ledger.
Money never touches a float. Amounts are DECIMAL(15,2) in Postgres, travel as decimal strings in
JSON, and are computed in Go with big.Rat. The rates themselves (flat monthly interest, daily
penalty with a cap, the loan multiplier, the due day) live in a settings table, so changing policy
is a data change rather than a deploy. Files are private: proof of transfer and ID cards are served
only through an authenticated endpoint, and an admin’s own verification proof is stored separately
from the member’s upload, so the two can never be confused for one another.
Contact
Interested in working together?
Tell me what you are building and what you need. A short note is enough.