CBS Core is a core banking system for Indonesian rural banks (BPR / BPRS),
covering conventional and syariah (UUS) business: tabungan, deposito, and
kredit/pembiayaan, on top of a strict double-entry general ledger with
maker-checker approval, EOD/EOM/EOY batch processing, PPAP/PPKA collectibility
and CKPN provisioning, and OJK reporting data. Design decisions are driven by
Indonesian banking regulation — notably POJK No. 1/2024 (asset quality),
and POJK No. 23/2024 with SEOJK No. 16/2024 (OJK reporting). The
regulatory basis, product decisions, and open debt live in
docs/KEPUTUSAN.md; deployment prerequisites live in
docs/DEPLOY.md. Not sure which document is authoritative for a
topic? Start at docs/INDEX.md (one topic, one canonical place).
Where code and docs disagree, the code and migrations are the source of truth.
cbs-core/
├── apps/
│ ├── api/ # Go REST API (module path: cbs-core/apps/core-api)
│ │ ├── cmd/server/ # API entrypoint
│ │ ├── cmd/backfill-name-tokens/
│ │ ├── cmd/reindex-customer-indexes/
│ │ └── internal/ # domain, repository, service, handler, middleware, config, crypto, ojkreport
│ └── web/ # Next.js 15 backoffice (package name: web)
│ └── src/app/(dashboard)/ # rekening, transaksi, kredit, pembiayaan, deposito, ppap, laporan, ...
├── packages/
│ ├── db-migrations/ # PostgreSQL migrations, numbered *.up.sql (+ symmetric *.down.sql since 000099)
│ └── shared-types/ # @cbs/shared-types — shared TypeScript contracts
├── deploy/ # Podman Quadlet units (cbs-api, cbs-web), Caddy config,
│ # and observability/ (shared VictoriaMetrics)
├── scripts/ # migrate.sh, preflight.sh, check-secrets.sh, deploy.sh
├── docs/ # peta dokumen: docs/INDEX.md
├── docker-compose.yml # local Postgres + Redis (see caveats below)
├── Makefile
├── go.work # Go workspace (use ./apps/api), Go 1.26.0
├── pnpm-workspace.yaml # pnpm workspace root
└── turbo.json
Toolchain as recorded in the repo:
| Component | Version | Source |
|---|---|---|
| Go | 1.26.0 | apps/api/go.mod, go.work (API image builder is golang:1.26-alpine) |
| Node | 22 (container image) | apps/web/Dockerfile; no .nvmrc or engines field is pinned |
| pnpm | 11.25.0 | package.json packageManager |
| PostgreSQL | 16 locally (docker-compose.yml), 18 in production |
docker-compose.yml, docs/DEPLOY.md |
| Turbo | ^2.4.4 | package.json |
docker-compose.yml starts PostgreSQL 16 and Redis. Two limitations are real
and should not be mistaken for a complete dev setup:
- It only applies migration
000001_init_cbs_schema.up.sql(initdb mount). A fresh database from Compose is therefore not fully migrated. - It starts a Redis container (
cbs-redis) that no application code uses.
# Start Postgres only; Compose also defines an unused Redis service.
docker compose up -d postgres
# or: make docker-up (starts both services)Apply the full migration set with the repo's own script:
# Defaults: container qouver-postgres, database cbs, owner qouver.
# Override the three variables to match the Compose database above:
CBS_DB_CONTAINER=cbs-postgres CBS_DB_NAME=cbs_db CBS_DB_OWNER=cbs_user \
scripts/migrate.sh
scripts/migrate.sh --dry-run # list pending migrations (still needs a reachable DB)
scripts/migrate.sh --container NAME # target another postgres container
scripts/migrate.sh --remote # run psql on the production host over SSHscripts/migrate.sh invokes psql inside a container through podman exec
(over SSH with --remote), one transaction per file (ON_ERROR_STOP=1, -1), and
records each applied *.up.sql in schema_migrations. Migrations are up-only by
default. New migrations are written with a symmetric *.down.sql (practice since
000099), and an opt-in rollback path is available via
scripts/migrate.sh --down --yes [N] (see docs/DEPLOY.md); it is never run
automatically. For production incidents, restoring a backup is still preferred.
Defaults in apps/api/internal/config/config.go match the database defined in
docker-compose.yml (localhost:5432) and listen on :8080.
cp .env.example .env # then edit — see the warning below
make dev-api # or: cd apps/api && go run cmd/server/main.goWarning about .env.example: it sets APP_ENV=production and does not
define ENCRYPTION_MASTER_KEY. With APP_ENV=production the API refuses to
start unless ENCRYPTION_MASTER_KEY (and a non-empty JWT_SECRET) are present.
For local development set APP_ENV=development and provide a base64-encoded
32-byte ENCRYPTION_MASTER_KEY if you need customer endpoints.
- Health check:
curl http://localhost:8080/healthz - API routes are under
/api/v1/...
apps/web is a pnpm workspace package. It talks to the API through a Next.js
same-origin rewrite (/api/:path* → INTERNAL_API_URL, default
http://cbs-api:8080), so set that variable for a local API.
pnpm install
INTERNAL_API_URL=http://localhost:8080 make dev
# or: cd apps/web && INTERNAL_API_URL=http://localhost:8080 pnpm devThe web app listens on http://localhost:3000.
| Service | Local | Production (host → container) |
|---|---|---|
| API | 8080 |
localhost:8095 → 8080 |
| Web | 3000 |
localhost:3005 → 3000 |
| PostgreSQL | 5432 |
container-internal, behind a reverse proxy |
cd apps/api && go test ./... # or: make test-api (runs with -v)Unit tests need no database and pass with go test ./....
Integration tests are skipped unless CBS_TEST_DB_DSN is set. Point it at a
disposable, migrated database (never production):
# Use the local database credential from docker-compose.yml.
CBS_TEST_DB_DSN='postgres://cbs_user:<password>@localhost:5432/cbs_db?sslmode=disable' \
go test ./...Known caveat: when all integration tests share one database,
TestIntegrasiOJKFormDaftarDanNPL can fail because it asserts a bank-wide NPL
ratio exactly while other tests insert loans into the same database; it passes
in isolation. This is a test-isolation weakness, not a feature collision.
This is a working system in active use, but it is not "enterprise-grade" or
"production-ready" by assertion: CI covers build, lint, and unit tests only
(integration tests need a database and are not run there), and the open items
below are release gates. Verify current status against
docs/KEPUTUSAN.md before relying on any claim.
Implemented and exercised
- Double-entry ledger engine (
Σ debits = Σ credits) withshopspring/decimalarithmetic, explicit SQL (no ORM), and PostgreSQL row locking. - Customer (CIF) management with field-level AES-256-GCM envelope encryption and a blind index for search; bcrypt passwords; httpOnly session cookies with CSRF double-submit; RBAC and branch scoping; login rate limiting.
- Accounts (savings/current/GL), teller transactions with idempotency keys,
time deposits, conventional loans and syariah financing mapping, maker-checker
workflow, EOD/EOM/EOY batch, PPAP/PPKA collectibility rules, CKPN, collateral,
OJK report data, and an append-only
audit_logs. - The migration chain from zero is clean and idempotent, and the API boots
against a migrated schema and serves
/healthzand login.
Present but not real integrations
POST /api/v1/integrations/slik/checkand/dukcapil/verifyexist, but are backed by mock gateways (NewMockSLIKGateway,NewMockDukcapilGateway). There is no live Dukcapil or SLIK connection.- Document endpoints (deposit/withdrawal slips, loan agreement, thermal receipt, passbook) return HTML print views, not server-rendered PDF. There is no PDF printer integration.
Known gaps / not finished
- CKPN production parameters (
ckpn.pd_frac.gol_1..5,ckpn.lgd_frac) are unset, so credit computations fail with a clear error instead of producing numbers; the PD/LGD methodology needs bank data. - The COA → OJK account mapping is still a draft, so reports are not ready for APOLO submission.
- Backups exist on the production host only (no offsite copy); recovery was drilled once, not on a schedule.
- Distributed tracing is implemented but opt-in and currently off (no collector
runs on the host). Metrics are live: the API exposes
/metrics(Prometheus format) and a shared VictoriaMetrics instance scrapes every project on the host. Seedeploy/observability/.
Switches intentionally left off (see docs/KEPUTUSAN.md)
ppap.collateral.enabled, ckpn.enabled, loan.restructure.loss.enabled, the
Pasal 23 (LPS placement) module, and password expiry
(auth.password_expiry_days = 0).
- Comments and docs are written in Indonesian, and they explain why a decision was made, not just what the code does. Keep that style.
- Migrations are up-only by default. Add a new
NNNNNN_description.up.sql; never edit a migration that has been applied. Pair new migrations with a symmetric.down.sql(repo practice since000099); an opt-in rollback exists viascripts/migrate.sh --down --yes [N]. A mistake in production is recovered from a backup. - Run the tests affected by your change (
go test ./...inapps/api). - Never commit secrets, credentials, host names, or IP addresses. Install
scripts/check-secrets.shas a pre-commit hook, or run it manually. - Do not push without the owner's approval. Pushing drives the production deploy, and migrations cannot be rolled back.