← All projects
Case study · Fintech · B2B · Mobile

A cash-control platform that tracks every dinar from the drawer to the treasury.

Al Burhan gives a four-branch retail group in Annaba one system for opening and closing the day, taking cash by denomination, selling with change, sending money by courier and closing with a blind reconciliation. Every write is server-side, append-only and auditable. Delivered as a signed beta in four weeks.
ClientEURL El Modjamaa El Tidjari Elborhane, Annaba
TimelineAugust 2026, four weeks to signed beta
PlatformAndroid, React Native (bare), Arabic RTL
StackReact Native 0.86, TypeScript, Zustand, Supabase Postgres
Al Burhan sign-in screen
25
tables, all append-only for money
15+
SECURITY DEFINER RPCs, zero direct writes
4
branches, 7 roles, 3 on mobile
4 wk
from spec to signed beta APK

Overview.

Who it is for, what was broken, what shipped.
The client

A retail group with four branches and one treasury

Cashiers, couriers and field collectors handle cash all day. A CFO, treasurer and branch heads need to know, at any moment, how much is where and who touched it.

The problem

Paper counts, typed totals, no trail

Closings took hours, totals were typed by hand, change and credit sales blurred the numbers, and disputes could not be settled because nothing was immutable.

The product

A denomination-first ledger the phone cannot lie to

An Arabic Android app for the three mobile roles, backed by a Postgres money engine that only accepts counts, computes everything server-side, and never edits history.

Screens.

The cashier's day, step by step. The app is Arabic-only by design.
One phone, several sensitive roles, two passwords
01 · Two-gate login

One phone, several sensitive roles, two passwords

The client asked for a second password check before anyone touches money. Sign-in has two gates: the central account, then a financial re-verification that routes the user straight to their post.

  • 1
    Central gateUsername is mapped to an email alias behind the scenes so cashiers never type an email. Standard Supabase Auth underneath.
  • 2
    Financial gateA real password re-verification, not a PIN. A single RPC then returns role, branch, cashbox and the open financial day atomically.
  • 3
    Automatic routingCashier, courier and field collector land on their own home. Admin, CFO, treasurer and branch-head accounts are web-only and get blocked on mobile by a device guard.
The financial day, at a glance
02 · Cashier home

The financial day, at a glance

A cashier's day has a lifecycle: open, operate, hand over, close. The home screen shows where the day stands and what is allowed right now.

  • 1
    Day statusOpen or closed, session A or B, and whether a CFO-approved partial close is pending.
  • 2
    Drawer compositionLive totals derived from denomination movements, never from a typed amount.
  • 3
    Quick actionsCash in, direct sale, change return, exchange, expenses and the journal, each gated by the day state.
Count pieces, not totals
03 · Denominations

Count pieces, not totals

The client forbade hand-typed totals. Every count in the app, from opening the day to swapping notes for coins, is entered per denomination, and the server computes the amounts.

  • 1
    Drawer composition, computedThe current drawer is derived from recorded movements and labelled as computed automatically. Nobody types a balance.
  • 2
    The full Algerian setEvery note and coin in circulation, with the 200-dinar paper note and the 200-dinar coin treated as different items.
  • 3
    Exchange without changing the totalBreak a large note into smaller ones, or the reverse, and the server checks that the pieces leaving the drawer actually exist before it accepts the swap.
  • 4
    Non-circulable protocolA damaged or suspect note triggers mandatory camera evidence, the holder's identity, a server-numbered protection receipt and a triple notification to client, branch head and CFO.
Every dinar in gets a reason
04 · Cash intake

Every dinar in gets a reason

Money entering the drawer must be classified: sales, debt settlement, other, or a collection on behalf of another branch.

  • 1
    Mandatory classificationThree-way split plus cross-branch collection. No uncategorised cash.
  • 2
    Cross-branch collectionWrites the cashbox entry and a pending inter-branch balance in one atomic call. Only the CFO can settle it.
  • 3
    Idempotent by designEach write carries a client-generated UUID with a unique constraint server-side, so a retried request on a bad connection can never post twice.
Two cash flows, one net amount, one receipt
05 · Direct sale with change

Two cash flows, one net amount, one receipt

A customer pays 2,000 for a 1,750 item. The drawer receives 2,000 and gives back 250, but the sale is 1,750. The app records both movements atomically.

  • 1
    Received and change counted separatelyBoth sides are denomination counts, so the drawer composition stays exact.
  • 2
    Net equals item valueThe server records IN and OUT movements together and nets them, with an auto-numbered receipt.
  • 3
    Change return hard-linkedA later change return must reference a parent operation and is capped by that operation's amount and by what the drawer actually holds.
An immutable record the cashier can read
06 · Journal and archive

An immutable record the cashier can read

Nothing is ever edited or deleted. Corrections are reverse entries linked to the original, and the journal shows the full story of the day.

  • 1
    Per-type identitiesEach operation type has its own visual identity so a shift can be scanned at a glance.
  • 2
    Day archiveSession A and session B grouped, with transfer status per handover.
  • 3
    Audit log behind itWho, what, when and from which device, written server-side for every mutation.
Declare first, then compare
07 · Blind reconciliation and closing

Declare first, then compare

At close, the cashier declares the commercial figures without seeing the counted cash. The server matches the declaration against the drawer and surfaces the difference.

  • 1
    Three steps, in orderCommercial reconciliation first, then the automatic count, then the declaration and close. The commercial figures are entered before the counted cash is revealed.
  • 2
    Deferred debtsCredit sales are declared separately and tracked as debts, not hidden in the cash figure.
  • 3
    Partial closeWith CFO permission, a mid-day close opens a second session so a handover does not stop trading.
Money in motion, expenses on order
08 · Transfers and expenses

Money in motion, expenses on order

Cash leaves the branch only through a courier chain, and only after the day is closed. Expenses leave the drawer only against orders the CFO has already approved.

  • 1
    Locked until closeThe transfer tab stays locked while the day is open. A drawer emptying request becomes available the moment the financial day is closed, for exactly the counted amount.
  • 2
    Transfer chainAssigned, picked up, delivered, received by treasury. Each step is a state transition the server validates, and the courier has its own screens for it.
  • 3
    Execution ordersA cashier can execute an approved expense only, photographing the invoice as evidence into a private storage bucket.
  • 4
    Field collectorsA third mobile role collects from clients in the field with the same denomination-first rules.
Haptics on every confirm
17-component design system
Emerald design language
Camera evidence to private storage
Demo mode for client walkthroughs
Voice-note driven change requests

Money rules.

Invariants the database enforces, whatever the app does.
Append-onlyTransactions and movements are never updated or deleted. A correction is a new, linked reverse entry.
Derived balancesBranch balances are computed from the ledger on read. There is no editable balance column anywhere.
Integer centimesNo floating point touches money. Every amount is an integer number of centimes.
Counts, not amountsClients send denomination counts. The server computes amounts and checks availability per denomination before any outflow.
Idempotency keysEvery mutation carries a client UUID with a unique constraint, so retries and offline replays cannot double-post.
Zero client writesRLS on all 25 tables with no write policies. All writes go through SECURITY DEFINER functions.

Architecture.

A thin native client and a Postgres that owns the rules.
Android app
React Native 0.86, bare CLI21 screens, 7 navigators · Zustand per-feature stores with derived drawer composition · NativeWind · React Navigation 7 · i18next with a 537-line Arabic locale, RTL throughout
Three mobile rolesCashier (15 screens), courier (transfer chain), field collector. Web-only roles are blocked by a device guard.
Camera evidenceInvoice and damaged-note photos uploaded to a private bucket with per-user folder policies
→
Supabase
Postgres money engine25 tables: branches, cashboxes, users, clients, specimens, financial_days, transactions, denominations_movements, rejected_notes, commercial_declarations, deferred_debts, transfers, authorizations, expense_requests, execution_orders, branch_ledger, audit_log, banks, loans, notifications…
15+ SECURITY DEFINER RPCsopen_financial_day · record_inflow · record_direct_sale · record_cross_branch_inflow · record_change_return · report_rejected_note · declare_commercial · close_financial_day · execute_partial_close · execute_expense_order · platform_login · courier chain
Auth + Storage + one Edge FunctionUsername-to-email alias, two-gate login, session in AsyncStorage · private evidence bucket · one-shot admin-seed-users function that disables itself
→
Operations
Signed APKSideloaded beta for the client, release signing kept out of the repo
PlannedSQLite offline queue reusing the idempotency keys · push notifications · web dashboards for CFO, treasurer and branch heads in a separate repo
Docs21 spec and decision documents, client cahier summaries and a field-feedback fix plan

Engineering deep dives.

Six decisions behind a ledger you can trust.

1. An immutable, double-entry-style money model

Problem
The records must be legally defensible. A balance that was edited in place proves nothing.
Approach
Append-only transactions and denomination movements. Corrections exist only as linked reverse entries. Balances are always derived from the branch ledger, never stored and updated. Amounts are integer centimes.
Trade-off
Every balance is a query over history. Heavier reads in exchange for tamper evidence and replayability.
Lives in: supabase/migrations/0003_transactions.sql

2. Server-authoritative denomination accounting

Problem
Cashiers count pieces, not totals, and the client forbade hand-typed amounts.
Approach
The app only ever sends denomination counts. The server computes amounts, validates drawer availability per denomination before any outflow such as change or exchange, and treats the paper 200 and the coin 200 as distinct.
Trade-off
Chattier RPC payloads, but a client device can never fabricate an amount.
Lives in: toServerDenoms in src/features/cashier/api.ts

3. Two-gate auth with automatic post routing

Problem
One physical phone, multiple sensitive roles, and a client demand for a second password before the financial workspace.
Approach
Central Supabase session, then a real signInWithPassword re-verification, then a platform_login RPC that returns role, branch, cashbox and open day atomically. The navigator routes by role and bounces web-only roles to a device guard.
Trade-off
An extra network round-trip at login, softened with a branded routing overlay.
Lives in: src/lib/mockAuth.ts, LoginScreen.tsx, RoutingOverlay.tsx

4. Idempotent financial writes, offline-ready

Problem
Branch networks drop mid-request. A retried 'record cash' must never post twice.
Approach
Every mutating RPC carries a client-generated UUID with a unique constraint on the server. The planned SQLite sync queue reuses the same key, so offline replay is safe by construction.
Trade-off
Idempotency plumbing on every call before the offline store even ships.
Lives in: newClientUuid in api.ts

5. Compound atomic operations

Problem
A direct sale with change moves two cash flows but nets one amount. Half-completed operations would corrupt the drawer.
Approach
A single RPC records the IN and OUT movements together with net equal to the item value and an auto-numbered receipt. Cross-branch collection uses the same pattern, double-writing the cashbox entry and an inter-branch pending ledger that only the CFO can settle.
Trade-off
Fatter RPCs, in exchange for operations that cannot half-complete.
Lives in: record_direct_sale, record_cross_branch_inflow

6. Zero-write RLS posture

Problem
A leaked client key must not allow any data write.
Approach
Row-level security on all 25 tables with no insert, update or delete policies for clients. Every write path is a SECURITY DEFINER function, and a hardening pass revoked the implicit public execute grant from every function after a security advisor warning.
Trade-off
All authorization logic lives in SQL, so function review becomes the security boundary.
Lives in: supabase/migrations/0006_rls.sql, docs/12

Cash flow.

From the drawer to the treasury, with a role at every step.
1 · OpenCashier counts the drawer by denomination. The server opens the financial day.
2 · OperateIntake, direct sales with change, exchanges, approved expenses. Every movement is an append-only row.
3 · DeclareBlind commercial declaration, deferred debts recorded separately.
4 · Hand overCourier picks up, delivers, treasury confirms. A state machine tracks the transfer.
5 · SettleCFO settles inter-branch balances and reviews the audit log and the ledger.
Cashier
  • Open and close the day
  • Intake, sales, change, exchange
  • Non-circulable protocol
Courier
  • Transfer task chain
  • Pickup and delivery proof
Field collector
  • Client collections in the field
  • Same denomination rules
CFO, treasurer, branch head
  • Permissions and settlements
  • Web dashboards (planned)
  • Blocked on mobile