Architecture overview

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.

Perfinia system diagram The Expo app on iOS and Android uses the shared core and a local SQLite store. It calls a Cloudflare Worker, which holds aggregator secrets, talks to PSD2 aggregators and banks, and stores data in D1 with the same schema. Device · iPhone and Android apps/mobile · Expo SDK 57 expo-router screens, design-system components packages/core · pure TypeScript öre money, payday periods, categorisation, safe-to-spend, budgets, sync, provider contract packages/db · Drizzle + SQLite expo-sqlite on device, field encryption, key in Keychain / Keystore Cloudflare · EU apps/worker Hono routes Cron syncs 4× daily Secrets server-side Same core code D1 (SQLite) Same Drizzle schema Household isolation in one repository layer PSD2 aggregator → Swedish banks BankID consent, read-only account information RemoteProvider

The layers

App

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.

Domain core

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.

Storage

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.

Backend

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.

Design system

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.