atomic is a personal finance app that puts everything about your money in one place. Instead of switching between multiple apps, users can track spending, set goals, and manage their finances through a single, simple interface.
The app was built with React Native and TypeScript, styled with Tailwind CSS via NativeWind v4, and is powered by Expo for a smooth deployment on both iOS and Android.
Development requires Node.js, access to a physical iOS or Android device, and git for version control. Install the Expo Go app on your testing device.
Our team uses mise to keep Node.js versions consistent. Follow their setup guide before continuing.
Clone the repository and enter the project directory:
git clone https://github.com/empiricalhq/atomic
cd atomicInstall dependencies. If using mise, it handles all required tools automatically:
mise installFor manual dependency management, use npm:
npm installStart the development server with cache clearing:
npx expo start --clearScan the QR code displayed in your terminal using Expo Go. The app will compile and deploy to your device. Expo Go provides hot reloading: press r in the terminal to force refresh during development.
The app is structured into four layers, each with a clear responsibility to keep data flow simple and predictable.
- UI (
app,src/components): Renders the interface and handles user input. Components only receive data and callbacks from hooks. They should avoid containing business logic. - Logic (
src/hooks): Manages state and data flow between UI and data layers. Custom hooks likeuseTransactionsanduseBudgetcontain domain logic and expose simple APIs for components. - Data (
src/api): Defines how the app talks to data sources. Services (e.g.,transactionService.ts) specify what data is needed, but not how it's retrieved, making it easy to swap backends. - Infrastructure (
src/services): Handles low-level operations such as file storage and device features. These services focus on implementation details and don't include domain logic.
Static configuration lives in src/constants as a single
source of truth for application settings.
The directory structure reflects this:
| Directory | Purpose | Key files | Notes |
|---|---|---|---|
app/ |
Expo Router entry point and navigation routes | _layout.tsx, (tabs)/ |
File structure maps to routes. Handles routing only. |
src/api/ |
Data abstraction and business logic | transactionService.ts |
Future home for REST/GraphQL integration. |
src/components/ |
Reusable React components | common/, layout/, feature directories |
Organized by reusability and feature domain. |
src/constants/ |
Static configuration data | categories.ts, settings.ts, theme.ts |
Single source of truth for app-wide constants. |
src/hooks/ |
Application state and domain logic | useTransactions, useBudget |
Core business logic encapsulation. |
src/services/ |
Infrastructure and low-level operations | storageService.ts |
Implementation details without domain knowledge. |
src/types/ |
TypeScript definitions | Type definitions for data models | Centralized typing for consistency. |
src/utils/ |
Pure helper functions | cn, formatters |
Framework-agnostic utilities. |
The app directory uses Expo Router's file-based routing where directory
structure determines navigation paths. Route groups like
(tabs) share layouts defined in
_layout.tsx.
Component organization in src/components follows a hierarchy
that balances reusability with feature cohesion. Generic components in
common/ provide building blocks, while
layout/ contains structural elements.
Feature-specific components group by domain, such as
budget/BudgetSummaryCard.tsx.
The application implements unidirectional data flow for predictable state management. Consider how transaction data appears on the home screen:
When app/(tabs)/index.tsx mounts, it calls the
useTransactions() hook to access transaction data. The hook's useEffect
triggers loadTransactions(), initiating data retrieval.
The loadTransactions() function calls
transactionService.getUserTransactions(userId) from the data layer. This
abstracts storage mechanisms from UI logic. The transaction service delegates to
storageService.getTransactions(userId) in the infrastructure layer, which
retrieves data from AsyncStorage.
Each layer adds appropriate transformations as data returns. The storage service
provides raw data, the transaction service applies business rules like sorting
and filtering, and the hook manages loading states and error handling. The
HomeScreen re-renders with updated data, passing it to components like
TransactionListItem.
This architecture isolates changes to specific layers. Switching from AsyncStorage to a REST API requires modifications only in the service layer, leaving hooks and components unchanged.
State management varies by scope and complexity. Component-level state uses
React's useState for data that doesn't cross component boundaries, including
modal visibility, form inputs, and temporary UI states. This keeps state close
to its usage without adding complexity.
Domain-specific state uses custom hooks like useTransactions and useBudget
in src/hooks.
The project currently avoids global state management, relying on prop passing and isolated hooks.