Web-based, single-tenant, production-grade accounting for Indonesian SMBs — a reimagining of Frappe Books on a modern web stack. First-class support for PPh 21 (TER), PPh 23, PPN, Faktur Pajak, Bukti Potong, SPT Masa filings and Coretax exports.
The authoritative engineering spec is SPEC.md. The UI design system is documented in docs/aurora-ui.md.
- Append-only general ledger with reversal-by-append (
is_cancelled+reverses_id). No row is ever mutated after posting. - Posting engine (
internal/accounting/posting) — every document type implementsBuildEntries+OnSubmit/OnCancelhooks. One code path for atomicity, balance, and reversal across Sales / Purchase / Payment / Journal / Payroll. - Moving-average inventory with
pg_advisory_xact_lock(hashtext(item || warehouse))for safe concurrent submits. - Indonesian tax built-in — PMK 168/2023 TER brackets for PPh 21, PPN 11 % e-Faktur with NSFP pool reservation, Coretax-compatible CSV.
- Role-based access control — resource:action permissions enforced at every endpoint.
- PDF rendering via a Gotenberg sidecar (HTML → PDF, no headless Chrome in-process).
- Aurora UI — calm pastel-aurora background, white card surfaces, ⌘K command palette, indigo accent.
| Layer | Choice |
|---|---|
| Backend | Go 1.23 (modular monolith) |
| HTTP | chi + huma v2 (OpenAPI 3.1) |
| ORM | Bun + pgx v5 |
| Migrations | goose (SQL) |
| Jobs | Asynq (Redis) |
| DB | PostgreSQL 16 |
| Cache / broker | Redis 7 |
| Frontend | Vite + React 19 + TS strict + TanStack Router / Query / Table / Form |
| Styling | Tailwind v4 (Aurora token set) |
| Gotenberg sidecar | |
| Reverse proxy | Caddy 2 |
| Money type | shopspring/decimal (Go) / string-decimal (wire) |
| Tests | testcontainers-go (Postgres + Redis) |
cp deploy/.env.example deploy/.env
# edit deploy/.env — at minimum set a 48-byte JWT_SECRET
openssl rand -base64 48
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up --buildThen open http://localhost:8080. Default bootstrap admin (dev only):
- Email:
admin@logica.dev - Password:
ChangeMe-LocalDevPasswordOver12Chars
OpenAPI: http://localhost:8080/api/v1/openapi.json · Stoplight: http://localhost:8080/api/v1/docs
# 1. Sidecars only (Postgres + Redis + Gotenberg)
docker compose -f deploy/docker-compose.yml up -d postgres redis gotenberg
# 2. Backend
go build -o /tmp/lb-server ./cmd/server
set -a; . deploy/.env; set +a
DATABASE_URL=postgres://logica:logica@localhost:5432/logica?sslmode=disable \
REDIS_URL=redis://localhost:6379/0 \
GOTENBERG_URL=http://localhost:3000 \
/tmp/lb-server --serve
# 3. Frontend dev server (hot reload, proxies /api → :8080)
cd web && npm install && npm run devOpen http://localhost:3001.
cmd/server/ Entrypoint — --serve, --worker, --migrate, --seed
internal/
app/ DI container + config
http/ chi server, huma mount, middleware, rate limit
auth/ Argon2id, JWT, refresh cookies, /auth/*
rbac/ Resolver + Guards middleware
accounting/
coa/ Chart of Accounts (tree, validation, type guards)
gl/ Posting-only GL repo (append + reverse-by-append)
posting/ Document interface + transactional engine
journal/ Free-form Journal Entry
sales/ Sales Invoice + line items + PPN allocator
purchase/ Purchase Bill + Input PPN
payment/ Receive / Pay + allocation against invoices/bills
inventory/ Stock Ledger Entries + moving-average valuation
item/ Item master
party/ Customer / Supplier / Both
tax/
ppn/ Output / input PPN allocator
pph21/ Payroll Run + Slip + PMK 168/2023 TER brackets
spt/ SPT Masa PPN + PPh 21 + e-Faktur CSV export
reports/ GL · TB · P&L · BS · AR/AP Aging · Sales/Purchase Register
pdf/ Gotenberg client + HTML templates
numbering/ SINV/BILL/PAY/JE/PR series resolver
db/ pgx pool, Bun, goose migrations, seed data
ratelimit/ Redis sliding window (Lua)
web/
app/
routes/ TanStack file-router (~22 routes)
components/
ui/ Aurora primitives (Button, Card, Input, Badge, Kbd, Table)
Sidebar.tsx 240px workspace nav with command-palette trigger
CommandPalette.tsx ⌘K palette
PageHeader.tsx Top-of-route header on the aurora gradient
lib/
queries/ TanStack Query hooks per domain
api/fetch.ts Bearer + 401-retry transport
auth.ts useMe / login / logout / refresh
format.ts id-ID currency / number / date helpers
styles/globals.css Tailwind v4 @theme — Aurora tokens
vite.config.ts Dev proxy /api → :8080
deploy/
docker-compose.yml Dev: app + postgres + redis + gotenberg
docker-compose.prod.yml Caddy reverse proxy, real domain, secrets via env
Caddyfile HTTPS termination
.env.example Document every required env var
- Money never crosses a JSON boundary as a float. Wire format is a string; in Go it's
shopspring/decimal; in the DB it'sNUMERIC(20, 2). - The general ledger is append-only. Cancel by appending the inverse with
is_cancelled = true+reverses_id. NeverUPDATE/DELETEgl_entry. - Every state transition runs in one transaction. The posting engine wraps
OnSubmit/OnCancelhooks and the GL writes together. - Permissions are enforced at the route, not the UI.
Guards.Require("resource", "action")middleware on every protected huma op. - Errors that the user can act on return 4xx. Cross-package sentinel errors map to 422/409 via per-handler
mapErr. Server faults are 500. - All time is UTC. Dates use
YYYY-MM-DD; the UI formats withIntl.DateTimeFormat("en-GB")for display.
GET /api/v1/openapi.json and /api/v1/docs are the source of truth.
Resource families: auth, accounts, parties, items, sales/invoices, purchase/bills, payments, journal-entries, inventory/{ledger,stock-balance}, tax/faktur-pajak, tax/payroll-runs, tax/employees, reports/*. Every list route accepts limit + offset + a filter set documented in OpenAPI.
# Unit + posting-engine integration (testcontainers-go boots a real Postgres)
go test ./...
# Frontend type check + build
cd web && npx tsc --noEmit && npx vite buildThe posting engine has end-to-end tests against a real Postgres for every doctype.
| Phase | Scope | Status |
|---|---|---|
| 0–2 | Foundations, auth, RBAC, masters | ✅ Done |
| 3 | Journal Entry + posting engine | ✅ Done |
| 4 | Sales / Purchase / Payments + GL reports | ✅ Done |
| 5 | Numbering, e-Faktur scaffolding | ✅ Done |
| 6 | Inventory + moving-average valuation | ✅ Done |
| 7 | Tax — PPN allocator + PPh 21 TER + SPT Masa | ✅ Done |
| 8 | Financial reports (TB/P&L/BS/Registers/SPT PPh 21) + CSV / PDF | ✅ Done |
| 8.5 | Aurora UI revamp | ✅ Done |
| 9 | Background jobs (Asynq), period close, audit log | 🛠️ Open issues |
| 10 | Bukti Potong PDF + Coretax XML / API | 🛠️ Open issues |
| 11 | Bank reconciliation, recurring docs, attachments | 🛠️ Open issues |
| 12 | Mobile responsive, i18n (id/en switch), dark mode | 🛠️ Open issues |
See Issues for the live punch list.
- Bootstrap admin credentials only fire when the user table is empty. Once seeded, those env vars do nothing. Change the password and disable seed credentials in production.
JWT_SECRETmust be ≥ 32 bytes. The server refuses to boot otherwise.- Refresh tokens are rotated on every refresh and stored in a
Secure; HttpOnly; SameSite=Laxcookie. - Argon2id parameters:
m=64 MiB, t=3, p=4. Tunable ininternal/auth/argon.go. gl_entryis append-only with a Postgres trigger preventingUPDATE/DELETE. Reversal is by inverse-append.
AGPL-3.0 — matches upstream Frappe Books (required for a derivative).