Case study · Contract · Jul 2026 – Oct 2026

Trackbill — Receipts, Security, and Reporting across Web and Mobile

Client
Inertia · Remote contract
Role
Contract Full-Stack Product Engineer
Timeline
Jul 2026 – Oct 2026

Contract product engineering for Trackbill, an AI-powered receipt and expense platform: receipt and WhatsApp reliability, unified routing, API-key security, reporting and exports, and mobile release readiness across three production codebases.

Merged pull requests: 45 web, 29 backend, and 32 mobile
106
Merged pull requests: 45 web, 29 backend, and 32 mobile
Validated multipart upload after moving OCR classification off the synchronous request path
36.01s
Validated multipart upload after moving OCR classification off the synchronous request path
Domains configured for Android/iOS App Links
2
Domains configured for Android/iOS App Links
Production codebases: Express/Prisma API, Next.js dashboard, Expo mobile app
3
Production codebases: Express/Prisma API, Next.js dashboard, Expo mobile app
Locales shipped with the unified-inbox setting, each covered by a locale-completeness test
15
Locales shipped with the unified-inbox setting, each covered by a locale-completeness test

Stack

Node.js · Express 4 · Prisma 5 · MySQL · TypeScript · Zod · Next.js 15 · React 19 · TanStack Query · Expo SDK 57 · React Native · Jest · Supertest · React Testing Library · GitHub Actions · EAS · Google Cloud Run

The brief

Trackbill is an AI-powered expense and receipt platform for small businesses in Asia-Pacific. Receipts arrive through messaging channels, email, a web dashboard, a mobile app, and a public API; classification feeds approvals, folders, reports, and accounting sync. From July through October 2026, I delivered product, reliability, and platform improvements across the Express/Prisma API, Next.js dashboard, and Expo/React Native mobile app, from schema and routes to interface, tests, and release tooling.

Three codebases, one slice at a time

The API mounts one protected router behind two front doors: first-party web and mobile clients authenticate with a Firebase ID token, third parties with an API key. Because the mobile app behaves like the web client, most mobile features needed little new backend, and a typical ticket touched the Prisma schema and migrations, Express controllers and routes, the Zod response schemas, the dashboard, and the Expo screens in one pass.

  • Mobile: trash and restore with swipe-to-delete, receipt thumbnails, sign-in polish and legal links, a development-environment badge, invite-link onboarding over universal links, Export Center parity with the web, a reproducible local iOS build with local signing, and the App Store icon.
  • Web: inline receipt editing, bulk category updates, bulk approvals with partial-failure handling, clearer duplicate-receipt results, debounced folder search and sorting by last upload, paginated user lists, inline seat editing, persisted report exports, a request-timeout policy, analytics tracking, and MRR reporting for the admin console.
  • Server: changes to existing endpoints across twelve route modules, plus new routes for API keys and phone linking, each with Supertest coverage.

API-key security lifecycle

Third-party integrations authenticate with API keys, and those keys had been stored in plaintext. I replaced that with a lifecycle in which the secret is 256 bits of randomness, shown exactly once at creation, and stored only as a SHA-256 hash with its last four characters kept for display.

  • Legacy keys migrate themselves: when a hash lookup misses, the server checks the legacy column, writes the hash, and clears the plaintext in place, so every existing integration kept working with no forced re-issue and no downtime.
  • Redaction is a property of the type. The public API-key schema is a Zod transform that strips the secret and emits only a masked value, and every response passes through the same sanitiser, so a plaintext key cannot leak through a list or read response even from a not-yet-migrated row.
  • Revocation is a soft delete that takes effect on the next request; each key records when it was last used; a per-user quota is enforced on both client and server; and the settings page gained create and revoke dialogs that clear the revealed secret from state as soon as it is copied.

Routing receipts from many channels into one inbox

By default the product filed each receipt by the channel it came through, so one person’s receipts scattered across a WhatsApp folder, an app folder, and more. I shipped a per-user setting that routes WhatsApp, LINE, and mobile uploads into a single inbox, across all three codebases in one ticket.

  • Folder resolution is find-or-create, and the API runs on several Cloud Run instances at once, so two first uploads arriving together could both try to create the inbox and one would fail. Resolution now runs under a per-user MySQL named advisory lock held inside one interactive transaction, so the lock is released on the same connection that took it and one user never blocks another.
  • Folder identity is a metadata key rather than a name, so renaming the inbox keeps routing intact and an existing folder with the default name is adopted instead of duplicated. A lock timeout fails before any credit is spent, a mid-transaction failure rolls back, and a failed classification refunds the OCR credit through an append-only ledger.
  • Neither client implements routing logic: the dashboard sends the folder in view, the app asks the API for its default folder, and the preference lives in one server-side column, so flipping it on the web changes the next mobile upload with no app release. The setting shipped in all fifteen locales with tests asserting every language carries the strings.

Receipt actions and upload reliability

WhatsApp receipt workflows need to remain correct when actions arrive late or more than once. I enforced a single valid Split/No action per receipt with atomic state commits, stale and duplicate-action rejection, localized feedback, and regression coverage across Meta and Evolution webhook paths.

  • Moved a 25–28 second OCR classification task off the synchronous upload request path.
  • Replaced a global 30-second cutoff with a 120-second default and validated a 36.01-second multipart upload flow.

Reporting and mobile release readiness

Expanded organization report history, OCR usage by channel, currency-aware MRR, cursor-paginated users, and subscription-seat editing. Mobile export workflows gained history, downloads, reruns, and CSV/XLSX/PDF support.

  • Supported reproducible iOS IPA builds, Sign in with Apple, and privacy configuration.
  • Configured Android/iOS App Links across two domains and fixed App Store assets and image orientation.

Quality across server, web, and mobile

Every slice shipped with tests in the layer it touched, and the routing work in particular is covered around its failure modes.

  • Server: Jest with Supertest at the route level, including tests that the advisory lock is released when resolution fails and that no folder is resolved when the lock cannot be acquired.
  • Web: Jest with React Testing Library, including an optimistic-UI test that the routing toggle reverts to its stored value and reports an error when the save is rejected.
  • Mobile: contract tests on Node’s built-in test runner against the API’s response shapes, catching client and server drift without a device farm. Continuous integration runs lint, type-check, tests, and an Expo prebuild for both platforms on every pull request.

Outcome

From July through October 2026, I contributed 106 merged pull requests: 45 web, 29 backend, and 32 mobile. The work covered receipt and WhatsApp reliability, unified routing across 15 languages, API-key security, reporting and export workflows, and Android/iOS release readiness. The API-key migration removed plaintext secrets without disrupting existing integrations, and the unified inbox brought receipts from multiple channels into one place.

Working on something similar?