<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Altaaf Hamod | MrVampCruz</title>
    <description>The latest articles on DEV Community by Altaaf Hamod | MrVampCruz (@mrvampcruz).</description>
    <link>https://dev.to/mrvampcruz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3826781%2F134e62a0-d02e-4df2-ac62-a4e73768d183.png</url>
      <title>DEV Community: Altaaf Hamod | MrVampCruz</title>
      <link>https://dev.to/mrvampcruz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mrvampcruz"/>
    <language>en</language>
    <item>
      <title>I shipped a 100% offline personal finance app as a solo dev — here's the full stack</title>
      <dc:creator>Altaaf Hamod | MrVampCruz</dc:creator>
      <pubDate>Wed, 08 Apr 2026 11:58:20 +0000</pubDate>
      <link>https://dev.to/mrvampcruz/i-shipped-a-100-offline-personal-finance-app-as-a-solo-dev-heres-the-full-stack-jnm</link>
      <guid>https://dev.to/mrvampcruz/i-shipped-a-100-offline-personal-finance-app-as-a-solo-dev-heres-the-full-stack-jnm</guid>
      <description>&lt;p&gt;After months of building in evenings and weekends, I shipped &lt;strong&gt;Budgetify&lt;/strong&gt; to the Google Play Store last week. It's a personal finance tracker built around one hard constraint: &lt;strong&gt;no servers, no accounts, no data ever leaving your device&lt;/strong&gt; (unless you explicitly back it up).&lt;/p&gt;

&lt;p&gt;This post is about the technical decisions that made that possible — and the tricky parts I didn't expect.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why build another finance app?
&lt;/h2&gt;

&lt;p&gt;Every finance app I tried either required an account, synced data to their servers by default, or locked basic features (like recurring transactions) behind $10+/month subscriptions.&lt;/p&gt;

&lt;p&gt;I wanted something that worked like a proper offline-first tool — fast, private, yours. So I built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React Native + Expo&lt;/strong&gt; (managed workflow)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expo Router&lt;/strong&gt; (file-based routing — &lt;code&gt;app/(tabs)/&lt;/code&gt; and &lt;code&gt;app/(screen)/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt; — all data, all local, &lt;code&gt;expo-sqlite&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RevenueCat&lt;/strong&gt; — subscription &amp;amp; IAP management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google AdMob&lt;/strong&gt; — free tier monetization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAS (Expo Application Services)&lt;/strong&gt; — builds &amp;amp; Play Store deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions + EAS Workflows&lt;/strong&gt; — CI/CD pipeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;expo-updates&lt;/code&gt;&lt;/strong&gt; — OTA updates for JS-layer changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;expo-in-app-updates&lt;/code&gt;&lt;/strong&gt; — Play Core in-app update prompts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;react-native-google-signin&lt;/code&gt;&lt;/strong&gt; — Google auth, proxied behind a single module&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;react-native-gifted-charts&lt;/code&gt;&lt;/strong&gt; — spending bar charts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dayjs&lt;/code&gt;&lt;/strong&gt; — all date handling (no &lt;code&gt;new Date()&lt;/code&gt; anywhere)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;react-i18next&lt;/code&gt;&lt;/strong&gt; — i18n (en + fr at launch, more locales planned)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The offline-first architecture
&lt;/h2&gt;

&lt;p&gt;All app data lives in a local SQLite database. The schema includes wallets, transactions (credit/debit/transfer), recurring transactions with occurrence tracking, category budgets, and app metadata.&lt;/p&gt;

&lt;p&gt;A few patterns that made this work cleanly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sequential DB queries, always.&lt;/strong&gt; SQLite on React Native doesn't handle concurrent writes well. Every query is &lt;code&gt;await&lt;/code&gt;-ed sequentially — &lt;code&gt;Promise.all&lt;/code&gt; on writes causes crashes. Learned this the hard way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transfer transactions use a &lt;code&gt;transfer_group_id&lt;/code&gt;.&lt;/strong&gt; Each transfer creates two rows — one &lt;code&gt;out&lt;/code&gt;, one &lt;code&gt;in&lt;/code&gt; — linked by a shared UUID. This keeps queries simple while making transfers fully reversible and trackable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-currency amounts are resolved with a COALESCE pattern:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount_in_wallet_currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;exchange_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This handles the case where exchange rates were configured at transaction time vs. not.&lt;/p&gt;




&lt;h2&gt;
  
  
  AES-256 encrypted backups
&lt;/h2&gt;

&lt;p&gt;Backup files are fully AES-256 encrypted before writing to disk or uploading to Google Drive. The encryption key is derived from a secret baked into the app at build time via &lt;code&gt;APP_SECRET&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One critical rule I set early:&lt;/strong&gt; &lt;code&gt;APP_SECRET&lt;/code&gt; must never change after the first production release. If it changes, every user's existing backup becomes unrestorable. This is the kind of constraint that's easy to forget months in when you're rotating keys.&lt;/p&gt;

&lt;p&gt;The Google Drive backup flow was the trickiest part — &lt;code&gt;@react-native-google-signin&lt;/code&gt; is powerful but has some sharp edges. I ended up isolating everything behind a single proxy module (&lt;code&gt;lib/googleDriveBackup.ts&lt;/code&gt;) and enforcing a rule: that package is never imported anywhere else in the codebase. All Google auth flows go through that one file.&lt;/p&gt;




&lt;h2&gt;
  
  
  CI/CD with EAS Workflows
&lt;/h2&gt;

&lt;p&gt;Two GitHub Actions workflows handle deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Push a tag matching &lt;code&gt;preview-*&lt;/code&gt; → EAS builds an APK for internal testing&lt;/li&gt;
&lt;li&gt;Push a tag matching &lt;code&gt;v*&lt;/code&gt; → EAS builds an AAB and submits to Play Store (alpha or production track)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OTA updates (JS-layer only) are triggered by including &lt;code&gt;[OTA]&lt;/code&gt; in the commit message — no rebuild needed for most changes.&lt;/p&gt;

&lt;p&gt;One thing worth knowing: &lt;strong&gt;environment variable changes always require a full EAS rebuild.&lt;/strong&gt; OTA can't patch &lt;code&gt;.env&lt;/code&gt; values since they're baked at build time. Same goes for app icons — they're native assets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Subscription model
&lt;/h2&gt;

&lt;p&gt;Four tiers via RevenueCat:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free (with ads)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ad-Free&lt;/td&gt;
&lt;td&gt;$0.99 one-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro Monthly&lt;/td&gt;
&lt;td&gt;$1.99/mo (7-day trial)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro Yearly&lt;/td&gt;
&lt;td&gt;$21.99/yr (14-day trial)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifetime Pro&lt;/td&gt;
&lt;td&gt;$89.99 one-time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The free tier is ad-supported via AdMob. Premium users see zero ads. I wrote a &lt;code&gt;BannerAdView&lt;/code&gt; component that auto-hides itself for any paid tier — screens don't need to know about subscription state directly.&lt;/p&gt;

&lt;p&gt;Feature gating uses a &lt;code&gt;can("feature")&lt;/code&gt; hook from &lt;code&gt;useSubscription()&lt;/code&gt;, and paywalled UI uses a &lt;code&gt;BlurredUpgradeOverlay&lt;/code&gt; component. Keeps the logic out of the screens themselves.&lt;/p&gt;




&lt;h2&gt;
  
  
  Localization
&lt;/h2&gt;

&lt;p&gt;The Play Store listing launched in 6 locales: &lt;code&gt;en-US&lt;/code&gt;, &lt;code&gt;fr-FR&lt;/code&gt;, &lt;code&gt;ar&lt;/code&gt;, &lt;code&gt;hi-IN&lt;/code&gt;, &lt;code&gt;pt-BR&lt;/code&gt;, &lt;code&gt;es-419&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One thing that caught me: &lt;strong&gt;Play Store descriptions are plain text only&lt;/strong&gt; — no Markdown, no bold, no dashes for bullets (use &lt;code&gt;•&lt;/code&gt; instead). And the 4,000-character limit counts CRLF line endings, not just LF. I now verify character counts programmatically before updating listings.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fingerprint / PIN lock&lt;/li&gt;
&lt;li&gt;Savings goals&lt;/li&gt;
&lt;li&gt;Wallet budgeting&lt;/li&gt;
&lt;li&gt;Home screen widget&lt;/li&gt;
&lt;li&gt;iOS (eventually)&lt;/li&gt;
&lt;li&gt;Financial Health Score&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;a href="https://play.google.com/store/apps/details?id=com.mrvampcruz.budgetify" rel="noopener noreferrer"&gt;Play Store&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 &lt;a href="https://budgetify.mrvampcruz.com" rel="noopener noreferrer"&gt;budgetify.mrvampcruz.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://www.reddit.com/r/Budgetify/" rel="noopener noreferrer"&gt;r/Budgetify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🚀 &lt;a href="https://www.producthunt.com/products/budgetify-by-mrvampcruz?launch=budgetify-by-mrvampcruz" rel="noopener noreferrer"&gt;Product Hunt&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy to go deeper on any part of this — the SQLite patterns, RevenueCat integration, EAS workflows, or the encryption approach. Ask away.&lt;/p&gt;

&lt;p&gt;— MrVampCruz&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;A few notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The "learned this the hard way" moments (SQLite concurrency, &lt;code&gt;APP_SECRET&lt;/code&gt;) read really well on Dev.to — devs appreciate honesty over polish&lt;/li&gt;
&lt;li&gt;The SQL snippet and the table give it a technical texture that fits the platform well&lt;/li&gt;
&lt;li&gt;Tag with &lt;code&gt;showdev&lt;/code&gt; — that tag gets strong traction for indie launch posts on Dev.to&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
  </channel>
</rss>
