How Perfinia is built
One TypeScript codebase from phone to server. The money logic lives in a pure core that knows nothing about screens or databases, so it is tested once and runs the same on iPhone, Android and Cloudflare.
The layers
apps/mobile: one Expo app with file-based routing in src/app. It uses only modules bundled in Expo Go, so the same build runs on iOS and Android without native projects in the repo.
packages/core: money in integer öre (never floats), payday periods with Swedish bank holidays, categorisation and learning rules, transfer matching, recurring detection, safe-to-spend, budgets, net worth, consent lifecycle and sync. No React Native imports.
packages/db: a Drizzle schema with portable migrations behind a Store interface. The same schema runs on expo-sqlite, bun:sqlite in tests and Cloudflare D1. Consent tokens use XChaCha20-Poly1305 field encryption.
apps/worker: a Cloudflare Worker on Hono that exposes the provider contract over HTTP, so aggregator secrets never ship in the app. Cron Triggers refresh data four times a day.
perfinia-design-system: brand book, logos and tokens.json. packages/design-tokens generates the app theme and checks the contrast promises; CI fails on drift.
Key decisions
Expo and TypeScript instead of Kotlin Multiplatform
The spec allowed either. One Expo codebase gives identical behaviour on iOS and Android, a path to a web client, and the same language on the server. Features that need native code (SQLCipher, widgets, certificate pinning) wait for an EAS development build, with no restructuring needed.
A provider contract, tested from both sides
Screens talk to an AccountDataProvider. The sandbox bank, the HTTP RemoteProvider
and the real aggregator adapter all pass the same contract suite, so moving from demo data to real banks
changes no screen code.
Cloudflare Workers and D1, with an exit to Postgres
Near-zero fixed cost for the beta, data kept in the EU. Because everything goes through Drizzle and the
Store interface, moving to Postgres is mostly a dialect change, and Hono runs on Node or Bun
when containers make more sense.
Households: iCloud sharing on Apple, codes everywhere else
On iPhone, partners join through the system sharing sheet (CloudKit CKShare), and a
family-shared Perfinia Plus suggests joining the household. Invite codes cover Android. Either way,
a single scopeToViewer function decides who sees which accounts.
Programming principles
Test-first from requirements
Every requirement in the spec has an ID such as FR-DASH-01 and Gherkin acceptance criteria. Test names carry the ID, and a traceability script fails CI if any MVP requirement has no test.
Property tests for money
fast-check generates thousands of random cases to check the invariants: splitting an amount never loses an öre, transfers between your own accounts net to zero, and payday periods follow each other with no gaps.
Journeys on a real simulator
Maestro flows sign up, connect a sandbox bank with BankID, review, budget, share a household and delete the account, end to end.
Tokens, not hex codes
Colours, type, spacing and radii come from one JSON file. The generator checks that every text colour holds 5:1 contrast in both themes.
Voice as a test
A copy test rejects exclamation marks, emoji and alarm words like "Warning" or "Failed" in both Swedish and English.
Every state, designed
Loading, empty, error, populated, hidden balances, large text and dark: each screen has all seven. Stale data stays visible with "Last updated", never removed.
The design system in one glance
Fjord teal is the brand and action colour. Status uses On track Over pace and always pairs colour with a word. Twelve category hues are named after Nordic things and assigned in a fixed order that stays distinguishable for red-green colour blindness.
This page uses the same tokens, and follows your system's light or dark appearance.