Money and Currency
How Jod stores money. Ratified 2026-07-27. The analysis behind it: ../91-rails/schema/schema-money-columns-idr-decision.md.
How to store money
Store money as a whole number of the currency's smallest unit. Use bigint. Never use float. Never use decimal for an amount the system adds, invoices, or pays out.
The smallest unit comes from ISO 4217. Every currency Jod trades in today — SGD, IDR, MYR, PHP, THB — has 100 minor units. So the stored number is the amount times 100.
- SGD 25.50 is stored as
2550 - IDR 1,400,000 is stored as
140000000
Name the column *_cents. Keep this name because every currency we use has 100 minor units. If we ever trade in a currency that has none (Vietnamese dong, Japanese yen), rename the column and add a minor-unit column to geo_countries at that point. Do not build a per-currency lookup before it is needed — a wrong exponent is a silent 100× error.
Always bigint, never integer
A Postgres integer holds up to 2,147,483,647. In cents that is SGD 21 million — which looks like plenty. In rupiah it is only 21 million rupiah, about SGD 1,500. A normal Indonesian invoice overflows it. bigint costs 4 extra bytes per row and removes the problem for every currency.
Every table that holds money holds a currency
A money number on its own means nothing. Every table with a money column carries a currency column with the ISO 4217 code. Snapshot tables copy the currency at write time and never change it. Never hardcode a currency in code (jodapp-api/AGENTS.md anti-pattern 19).
What this rule does not cover
- Rates and percentages. Internal rates are integer basis points (
_bps). Rates exchanged with external systems (tax rates for IRAS / Xero) aredecimal(_rate). Seejodapp-api/db/AGENTS.md→ Rate Columns. - Credit counts. A credit is a unit, not money. Count credits with
bigint, no_centssuffix (billing_entitlement_balances.units_available). - Advertised pay on a listing. A marketing number, not money the system moves.
listings_jobs.pay_from/pay_tostay as they are.