Launching to app stores

My Finances

A personal-finance app with end-to-end encryption and a zero-knowledge backend: your financial data never leaves your device unencrypted.

React NativeTypeScriptNode.jsMongoDBVercelE2EE

This project was born from a personal need: I wanted to build the habit of managing my finances and couldn't find an app I fully trusted with my data. So I built it. My own product, designed and built end to end: a cross-platform mobile app to manage income and expenses, backed by a custom API. The founding premise was radical — that not even I, as the server's owner, could read a user's financial data.

The problem and the privacy thesis

Most finance apps store your records in plaintext on their servers. Here the design choice was the opposite: privacy by design. All encryption happens on the device, and the server only stores opaque blobs it cannot decrypt. The result is a zero-knowledge backend: even if the database were breached, the data would be unreadable.

Architecture

Two independent repositories, connected by a REST API and unified by a shared cryptographic model.

Mobile app (React Native · Expo)

Built with Expo SDK 55, React Native, and TypeScript. File-based routing with expo-router, state with Zustand, internationalization (en/es), animations with Reanimated, and secure storage in Keychain/Keystore. All encryption runs client-side via the Web Crypto API.

Backend API (Express · TypeScript)

Node.js + Express + TypeScript over MongoDB Atlas, deployed on Vercel. Layered architecture (controllers → services → repositories → models) with dependency injection, Zod validation, and Swagger docs. The server stores encrypted material but never processes it.

Offline-first approach

The app is built to work without a connection: the device is the source of truth and the cloud acts as an encrypted backup that syncs when the network is available.

How it was achieved

Records are read and written first to a local store on the device, so the UI responds instantly even with no connection. Because each record is an opaque encrypted blob —identical on the device and on the server— the same data travels without transformation: the app works against the local cache and syncs with the API opportunistically once the network returns. Each operation carries a unique id, so re-sending it is idempotent and never duplicates a record.

Security in offline mode

The local cache never stores plaintext: it holds the same end-to-end encrypted blobs, and keys stay in the system's secure storage (Keychain/Keystore). On top of that, every sensitive operation —creating, editing, or deleting a record— requires biometric authorization (fingerprint) before it's applied, even offline; and when syncing, each request is signed with HMAC within an anti-replay window.

Challenges migrating to offline-first

The biggest challenge was keeping encryption and state consistent between local and remote. It required migrating the data model so the client fully owned the content —moving sensitive fields inside a single encrypted payload—, defining how to reconcile changes and resolve conflicts when the same record is edited at different times, and preserving the security guarantees (biometric authorization and signed requests) for actions created offline and applied later.

Security in depth

The heart of the project. Defense in depth across every layer: identity, data, and transport.

End-to-end encryption

At sign-up, the device generates a 2048-bit RSA-OAEP keypair. Each record is encrypted with a hybrid scheme: an ephemeral AES-256-GCM key encrypts the content, and that key is encrypted with the user's RSA public key. The server only ever receives a base64 blob it cannot open.

Key management & recovery

The private key is encrypted with AES-256-GCM, deriving the key from your password via PBKDF2 (100,000 iterations, SHA-256, random salt and IV). A second copy is stored encrypted with your recovery passphrase, allowing account recovery without the server ever knowing the key and avoiding the deadlock of losing password and device at once.

Transport integrity

Every authenticated request is signed with HMAC-SHA256 over sessionId:timestamp, using a device secret that rotates on each login, with a ±5-minute validity window to stop replay attacks. Access JWTs last 1 hour and are refreshed automatically and queued via a refresh token.

Native security modules (iOS & Android)

I wrote a custom native module (Kotlin and Swift) that hardens the app at runtime: it detects compromised devices —root on Android, jailbreak on iOS—, an attached debugger, and whether it's running on an emulator or simulator, and blocks startup in production when something is off. It also implements iOS App Attest (Secure Enclave) which, together with Play Integrity on Android, lets the API require that every request comes from the genuine, unmodified app.

Defense in depth

Password, PIN, and biometrics; email verification to register or reset a device; cryptographic device binding; account lockout against brute force (5 login or 3 PIN attempts → 30 minutes); auditing of critical actions with geolocation; and app attestation (Play Integrity / Apple App Attest) to ensure only the genuine app talks to the API. A documented security audit hardened the system, resolving nearly 18 findings.

Stack & practices

Mobile app

Expo SDK 55React NativeTypeScriptexpo-routerZustandi18nextReanimatedexpo-secure-storeexpo-local-authenticationWeb Crypto API

Backend

Node.jsExpressTypeScriptMongoDB AtlasMongooseZodSwaggerVercelJWTDependency Injection

Security

RSA-OAEP 2048AES-256-GCMPBKDF2HMAC-SHA256Zero-KnowledgeRecovery CodesNative (Kotlin/Swift)App AttestPlay IntegrityJailbreak/Root Detection

Quality & DevOps

Huskylint-stagedJestsupertestCircleCIEAS BuildESLintPrettier

Gallery

How it looks and how it works. (Screenshots and GIFs of the main flows.)

Screenshots

Placeholder — screenshots and GIFs coming soon.IMG 1
Placeholder — screenshots and GIFs coming soon.IMG 2
Placeholder — screenshots and GIFs coming soon.IMG 3
Placeholder — screenshots and GIFs coming soon.IMG 4

Flows in action

Placeholder — screenshots and GIFs coming soon.GIF 1
Placeholder — screenshots and GIFs coming soon.GIF 2
Placeholder — screenshots and GIFs coming soon.GIF 3

Current status

The project is in its launch and store-deployment phase. There's no public link to share yet, but you can see screenshots of how it looks and GIFs of how it works in the gallery above.

Want to talk about the project?

I'm happy to dive into the technical decisions or walk you through a demo.

Contact me