CONV · Conventions
The rules every table follows: how money, phones and dates are stored, what is never deleted, and how the database refuses. Code CONV.
Answered questions
Section titled “Answered questions”- DOC-Q1 (search_path): a new rule, R-CONV-15, below.
- DOC-Q5 (which phone columns): R-CONV-07 covers every phone column, the four that V13 left unchecked included. They are listed under the rule.
- DOC-Q7 (dates defaulting to the server's day): registered_on, started_on and issued_on are business dates, so R-CONV-10 covers them. Their default is the pool's date, not the server's.
- DOC-Q8 (unknown setting name, bad counter period): neither becomes a rule. They catch a caller's mistake, not a business rule, so their refusals stay under R-CONV-13.
R-CONV-01 · Money is stored in whole rupiah as bigint, never as a fraction or a float
Section titled “R-CONV-01 · Money is stored in whole rupiah as bigint, never as a fraction or a float”- Status: planned
- Example: given a rent charge of Rp 150.000, when it is saved, then amount holds 150000 as a bigint, and a money column declared as numeric, real or double precision is a defect.
- Refusal: none: a fraction can't be stored in a bigint column.
- Who: every writer.
- Source: [Ref] Database rules and conventions, Money.
R-CONV-02 · A money amount is never negative
Section titled “R-CONV-02 · A money amount is never negative”- Status: planned
- Example: given a pricing group, when its admin_fee is saved as -50000, then the row is refused. The same holds for rates, costs, fees, shares and totals.
- Refusal: none: check_violation (23514).
- Who: every writer.
- Source: [Ref] Data model V13, the ≥ 0 checks on money columns.
R-CONV-03 · A period never ends before it starts
Section titled “R-CONV-03 · A period never ends before it starts”- Status: planned
- Example: given an insurance policy starting on 2026-11-01, when ends_on is saved as 2026-10-31, then the row is refused. The same holds for rentals, maintenance records and payouts.
- Refusal: none: check_violation (23514).
- Who: every writer.
- Source: [Ref] Data model V13: rentals, insurance_policies, maintenance_records, payouts.
R-CONV-04 · A due date is never before the date the charge or letter was raised
Section titled “R-CONV-04 · A due date is never before the date the charge or letter was raised”- Status: planned
- Example: given a charge dated 2026-10-07, when its due_date is saved as 2026-10-06, then the row is refused. The same holds for a debt letter's due_on against its issued_on.
- Refusal: none: check_violation (23514).
- Who: every writer.
- Source: [Ref] Data model V13: charges, debt_letters.
R-CONV-05 · A percentage reading (fuel level, battery) is between 0 and 100
Section titled “R-CONV-05 · A percentage reading (fuel level, battery) is between 0 and 100”- Status: planned
- Example: given an inspection, when fuel_level is saved as 120, then the row is refused.
- Refusal: none: check_violation (23514).
- Who: every writer.
- Source: [Ref] Data model V13: inspections.
R-CONV-06 · An odometer or kilometre reading is never negative
Section titled “R-CONV-06 · An odometer or kilometre reading is never negative”- Status: planned
- Example: given a car, when odometer_km is saved as -10, then the row is refused. The same holds for inspections and maintenance records.
- Refusal: none: check_violation (23514).
- Who: every writer.
- Source: [Ref] Data model V13: vehicles, inspections, maintenance_records.
R-CONV-07 · A phone number is stored in E.164 form, such as +6281234567890
Section titled “R-CONV-07 · A phone number is stored in E.164 form, such as +6281234567890”- Status: planned
- Example: given a garage, when its phone is saved as 0812-3456-7890, then the row is refused; +6281234567890 is accepted.
- Refusal: none: check_violation (23514).
- Who: every writer. The data phase normalizes v1's numbers before they are loaded.
- Source: [Ref] Database rules and conventions, Normalized input. The columns: drivers.phone, driver_emergency_contacts.phone, queue_tickets.phone, profiles.phone, rental_companies.contact_phone, garages.phone and insurers.phone. V13 checked only the first three; the other four are added (DOC-Q5). Any phone column added later follows the rule.
R-CONV-08 · Every mutable table has created_at and updated_at
Section titled “R-CONV-08 · Every mutable table has created_at and updated_at”- Status: planned
- Example: given any table whose rows can be updated, when its columns are listed, then created_at and updated_at are both there, as timestamptz.
- Refusal: none: this describes the schema.
- Who: every table. Append-only tables (GPS positions, events, the audit log) carry their own time column instead.
- Source: D9 (P14).
R-CONV-09 · The database sets updated_at on every update, whatever the client sends
Section titled “R-CONV-09 · The database sets updated_at on every update, whatever the client sends”- Status: planned
- Example: given a driver updated at 10:00, when a client updates the row at 10:05 and sends updated_at = 2020-01-01, then updated_at reads 10:05.
- Refusal: none: the value is overwritten, not refused.
- Who: every writer.
- Source: D9 (P14); [Ref] Database rules and conventions, Timestamps.
R-CONV-10 · Business dates (charge, queue and due dates, day counts) are computed in the pool's time zone
Section titled “R-CONV-10 · Business dates (charge, queue and due dates, day counts) are computed in the pool's time zone”- Status: planned
- Example: given a pool in Asia/Makassar (WITA), when a charge is raised at 23:30 WITA on 7 October, which is 15:30 UTC, then its charge_date is 2026-10-07, and at 00:30 WITA on 8 October it is 2026-10-08.
- Refusal: none.
- Who: every writer; app.pool_today(pool) gives the date.
- Source: D9 (P11). Also covered (DOC-Q7): drivers.registered_on (the driver's pool), maintenance_records.started_on (the car's home pool) and debt_letters.issued_on (the driver's pool). In V13 they default to the server's date, which is UTC on Supabase; they default to the pool's date instead.
R-CONV-11 · A row can't be deleted while business rows reference it
Section titled “R-CONV-11 · A row can't be deleted while business rows reference it”- Status: planned
- Example: given a car with a rental, when someone deletes the car, then the delete is refused. Photos, items and other pure children go with their parent.
- Refusal: none: foreign_key_violation (23503).
- Who: every writer.
- Source: D9 (P13); [Ref] Database rules and conventions, No hard deletes.
R-CONV-12 · Every single-column foreign key has an index
Section titled “R-CONV-12 · Every single-column foreign key has an index”- Status: planned
- Example: given rentals.vehicle_id points at vehicles, when the indexes on rentals are listed, then one starts with vehicle_id.
- Refusal: none: this describes the schema.
- Who: every table.
- Source: [Ref] Database rules and conventions, Indexes.
R-CONV-13 · Every refusal from a database function or trigger raises a stable key named domain.reason, in lower case
Section titled “R-CONV-13 · Every refusal from a database function or trigger raises a stable key named domain.reason, in lower case”- Status: planned
- Example: given a car at pool PML and a driver at another pool, when the rental is booked, then the error message is exactly rental.cross_pool. A key that isn't registered raises internal.unknown_error_key instead.
- Refusal: internal.unknown_error_key, when a function tries to raise an unregistered key.
- Who: every database function and trigger. Argument checks that catch a caller's mistake (settings.unknown_key, settings.pool_not_found, document.bad_period, document.wrong_organization, vehicle.not_found, driver.not_found, profile.not_found) follow this rule and have no rule of their own (DOC-Q8).
- Source: D10.
R-CONV-14 · A refusal's detail is a JSON object holding the values its message needs
Section titled “R-CONV-14 · A refusal's detail is a JSON object holding the values its message needs”- Status: planned
- Example: given an allocation of Rp 200.000 against a charge with Rp 150.000 open, when it is saved, then the detail is {"charge_id": "…", "balance": 150000, "amount": 200000}.
- Refusal: none: this describes every refusal.
- Who: every database function and trigger.
- Source: D10.
R-CONV-15 · Every function that runs as its owner sets a fixed search_path
Section titled “R-CONV-15 · Every function that runs as its owner sets a fixed search_path”- Status: planned
- Example: given app.set_vehicle_status, which is security definer, when its definition is read, then it sets search_path to a fixed list, so a caller's own schema can't swap a table or function it uses.
- Refusal: none: this describes the schema.
- Who: every security definer function.
- Source: D3 (rules run in security definer functions); Supabase's database linter, lint 0011 function_search_path_mutable. Added for DOC-Q1.