Case study
SonoWise | Sổ Nợ Online
An offline-first debt ledger app: track what people owe you, record repayments and see due dates, with no bank sync and no account needed.
- To be updated
- To be updated
- Angular · NestJS · Prisma · PostgreSQL · Redis · NATS · AWS S3 · Resend · React Native · Expo · expo-sqlite · Drizzle ORM · TypeScript
- 2026

Overview
SonoWise (SONO) is a Vietnamese-first app for managing debt ledgers ("sổ ghi nợ") between people, plus personal expense tracking. It started as a web product, then grew into a mobile platform.
The first version was a web system that tracked debts and synced bank transactions from third parties, so that an incoming transfer could mark a debt as paid. This second phase rebuilds the product as a mobile app that runs fully offline. It drops bank sync, because connecting to bank data carried too much risk for a tool that holds personal money records.
Challenge
The old system solved a real problem, but its design had three limits:
- Money state depended on the network. Recording a debt or a payment required the API. Without a connection, the user could not work.
- Bank sync was the core automation, and also the main risk. Casso and SePay webhooks wrote payments into the ledger automatically. That made the app's accuracy depend on a third party's data and on a payment flow the user could not fully verify.
- A debt tracker is used at the point of a transaction. People note a loan or a repayment at the market, in a shop, or on the street. The app had to be fast and usable with one hand, with no login in the way.
The new version had to keep the ledger model (ledgers, people, debts, payments, reminders), move it on-device, and keep the same Vietnamese-first experience.
Approach
Reading the old system first. The API is a NestJS service on PostgreSQL with Prisma. It has workspaces, debtors, debts, incoming payments, reminders, audit logs and shared account balances. Overdue reminders run as a background job, and email goes through Resend. The web client is Angular 18 with ng-zorro. I used the business rules from this backend as the reference for the local version. I did not port its HTTP and state layers.
Local database as the source of truth. The mobile app uses a single SQLite connection (expo-sqlite) with Drizzle ORM. Change listening is on, so screens update when rows change through useLiveQuery. Nothing is copied into a separate store. Schema changes are generated Drizzle migrations, and they run on boot behind the splash screen, so the first frame already shows the current data.
Stack without Redux. Phase 1 makes no API calls, so the app uses React context for theme, language and the active ledger. RxJS is kept for observables where they help. Forms use react-hook-form with Zod, and long lists use @shopify/flash-list.
Design as the contract. The screens come from the SONO Mobile V2 Figma file, which covers onboarding, ledger creation, debt lists and detail, payments, contacts, calendar reminders, expense tracking and receipt sharing. Colours are semantic tokens, and the palette is the only place with hex values.
Accessibility checked in code. The brand orange does not reach WCAG AA as text on white (about 2.6:1). I added a darker text colour for orange labels. A contrast test fails the build if any text token pair falls below AA. The brand gradient button is the one open exception, and it needs a design decision.
Vietnamese first, English ready. The Vietnamese dictionary is the reference. Every key added there must also exist in the English dictionary, and an i18n test enforces this. Light and dark themes switch through a root CSS-variable swap, so components do not need separate dark-mode styles.
Process. Each feature moves through a requirement, a design, a task list and a TDD build step, each with its own document. This kept the foundation work and the first features traceable.
Outcome
Phase 1 is built around offline-first ledgers, debts, payments and reminders, with bank sync removed on purpose.
- The foundation is in place: tokens, theme and contrast tests, i18n, the migration pipeline, and the onboarding flow with splash and first-run ledger creation.
- The local data layer runs on expo-sqlite with migrations applied on boot.
To be updated.
