Skip to content
Rafi Akmal Widikta

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.

Back to top