Privacy Policy

Privacy & Security White Paper

Every claim below was verified directly against Payd's source code. This document is meant to be checkable, not just readable.

Payd – Paycheck Planner Bundle ID: com.paydplanner.app Document version: 2.0 · Last updated: September 22, 2026 Status: Living document — updated whenever Payd's data handling changes


How to read this document

Every factual claim below was verified directly against Payd's source code or against the current, documented behavior of a named third-party service, on the date this document was last updated. Where a claim depends on a third party's own policies (RevenueCat, Apple, Google), we link to their documentation rather than restate it, because those policies can change on their own schedule.

This document does not use the phrase "bank-level encryption" or similar marketing shorthand. Where we describe encryption, we name the exact algorithm. Where we describe a limitation, we state it plainly rather than omit it.

If anything in this document is ever found to be inaccurate, we will correct it and note the correction in the changelog at the bottom of this page.


1. Executive Summary

Payd is a paycheck-based budgeting app that does not connect to your bank. There is no bank login, no account aggregator (Plaid, Yodlee, Finicity, or similar), and no Payd server that stores your financial data. Every bill, balance, paycheck, and goal you enter is stored locally on your device, using the operating system's standard local storage.

Only one kind of network request originates in Payd's own code: a stock ticker symbol sent to a quote provider, and only if you add a holding to the Portfolio tab. Buying a subscription also causes network activity, but it happens inside Apple's or Google's own store client, not in Payd — there is no third-party purchase SDK in the app and no Payd server anywhere. Section 5 describes both precisely. Outside of them, Payd has no way to see, transmit, or store your financial information, because the app has no backend capable of receiving it.

This architecture is a tradeoff, not a marketing claim. It means Payd cannot auto-categorize transactions from your bank the way Copilot or Monarch can, and it means there is no cloud sync between your devices unless you do it yourself via encrypted backup (Section 8) or the optional Partner Sync feature (Section 6). What it buys in return: there is no Payd database to breach, because there is no Payd database.


2. Design Philosophy

Most budgeting apps ask you to link a bank account through an aggregator so the app can pull transactions automatically. That convenience comes with a tradeoff: your login credentials, transaction history, and account balances pass through and are stored by a third party you didn't choose and likely haven't read the terms of.

Payd takes the opposite default. You tell the app what you earn and what you owe. In exchange for a small amount of manual entry — reduced by features like PDF bank-statement import (Section 6), which detects recurring bills from a statement you choose to upload once, without ever transmitting that statement anywhere — your financial picture never has to leave your phone.

This is a deliberate product decision, not a limitation we're working around. It is why the architecture described in this document is possible at all.


3. What Data Exists, and Where It Lives

Everything you enter into Payd is stored using your device's local app storage (AsyncStorage, the standard React Native/Expo local storage layer used by the majority of mobile apps). Nothing here is a Payd server — it is a set of files on your device, inside a storage area the operating system reserves for Payd and does not share with other apps.

Data category Storage key Ever leaves the device?
Pay schedule, bills, balances, spend log, budgets payd_data No
Tax / paystub entries payd_taxes No
Investment portfolio holdings payd_portfolio No
Savings goals & contributions payd_savings No
Theme & appearance settings payd_theme No
Subscription status (a local cache of "is Pro active", plus any temporary Pro you've unlocked) payd_entitlements No — the subscription record itself belongs to Apple or Google, who processed the purchase (Section 6.1). Payd sends them nothing to maintain it
Cashflow chart starting balances payd_starting_balances No
Payday confirmation state payd_payday_confirmation No
Last payday whose savings auto-contributions were applied — one YYYY-MM-DD date, so a payday's contributions can never be applied twice payd_last_auto_contribution No
Home screen widget data payd_widget_data No — and while "Hide amounts on widgets" is on (the default), no money value is written to this key at all: it holds only your next payday, the days until it, and the names and due dates of upcoming bills
Daily bill-checklist state (one key per day; keys older than 7 days are deleted automatically when the Today screen loads) payd_checklist_* No
Stock-quote provider API keys (optional, user-entered) payd_api_keys Only to that provider, as part of your own quote requests — see Section 6.2
Stock-quote provider API usage counters payd_api_usage No
Cached historical-return calculations (Investment Calculator's opt-in past-performance projection) payd_hist_returns No — this is a local cache of results already fetched under Section 5.1
App Lock on/off preference (optional Face ID / passcode gate) payd_app_lock No — and note this stores only the on/off setting. Biometric data and your device passcode never reach Payd at all; the operating system performs the authentication and returns only a yes/no (Section 7)
App Store review-prompt state payd_review_state No
Last local backup timestamp payd_last_backup_at No

Nothing in this table is transmitted to Payd, because Payd has no server to receive it. There is no account system, no login, and no user ID that identifies you to us — we don't have a "your data" to look up, because we never receive it.


4. What "Local Only" Actually Means, Technically

To be precise about a claim that's easy to overstate: "local only" means Payd's own code never makes a network request containing your financial data. It does not mean your data is invisible to the operating system, to other software running with elevated privileges on a compromised device, or to anyone with unlocked physical access to your phone.

Local storage on iOS and Android benefits from the same OS-level protections as any other app's local data — iOS Data Protection and Android's file-based encryption, both of which are active on a device protected by a passcode, PIN, or biometric lock. Payd does not layer additional encryption on top of this for day-to-day app data, the same way most note-taking or to-do apps don't. If your device itself is compromised (jailbroken, rooted, or unlocked without a passcode), local app storage becomes more exposed — this is true of Payd and true of nearly every app on your phone that isn't using a hardware-backed secure enclave for every field, which comes with real usability tradeoffs most apps (including Payd, today) don't make.

Where we do apply real encryption — your optional local backup file — is described in Section 8, with the exact algorithm named.


5. What Touches the Network

One request class originates in Payd's own code (5.1). One more involves the network without Payd making the request (5.2). Nothing else in the app calls out to the internet.

5.1 Stock price lookups (Portfolio tab, optional)

If you add a stock holding to the Portfolio tab, Payd fetches a current price by sending the ticker symbol (e.g. AAPL) to one of three providers, tried in order: Yahoo Finance's public quote endpoint, then Finnhub, then Alpha Vantage (the latter two only if you've supplied your own free API key for them). This request contains only the ticker symbol — no account information, no identifier tied to you, nothing from any other part of the app. It fires when you pull to refresh, and — if you turn on the "Auto" toggle in the Portfolio tab, which is off by default — at most once per calendar day when the app is opened or brought to the foreground, whether or not that attempt succeeds. If you never open the Portfolio tab or add a holding, this request never fires.

One related request exists outside the Portfolio tab: when you enter a Finnhub or Alpha Vantage API key in Settings and tap "Test," Payd makes a single validation request to that provider containing a fixed test ticker (AAPL) and the key you just entered — nothing else. This is the same class of request as a quote lookup, fires only when you explicitly test a key, and goes only to the provider the key belongs to.

The Investment Calculator inside the Portfolio tab includes an opt-in checkbox, off by default: "Base on my holdings' past performance." If you tick it, Payd fetches up to 5 years of monthly closing prices for each of your holdings' tickers — from Yahoo Finance's chart endpoint, then Alpha Vantage if you've supplied your own key (Finnhub is not used for this request; its free tier does not offer historical price series) — and computes each holding's annualized return, then weights those returns by position size to fill in the calculator's Annual Return field. This is the same ticker-only request class as the rest of this section: no account information, no identifier tied to you, nothing else from the app. It fires only when you tick the box, never automatically and never in the background, with an 8-second timeout per request and a 30-second ceiling on the whole run, and the results are cached locally for 7 days (payd_hist_returns, Section 3) so reopening the calculator doesn't re-fetch. Unticking the box unlocks the Annual Return field for manual entry and has no further network effect. The calculator always shows the result alongside a disclaimer: "Past performance does not guarantee future results — this is an estimate for planning, not investment advice."

5.2 Subscription purchases (Apple and Google, through the operating system)

Buying or restoring Payd Pro talks to the App Store or Google Play. To be precise about who makes that request: Payd calls the platform's own purchase API — StoreKit 2 on iOS, Google Play Billing on Android, reached through the expo-iap library — and the operating system's store client performs the network exchange with Apple or Google. Payd has no purchase SDK from any third party, no API key for one, and no server of its own in this path. This is described in full in Section 6.1.

5.3 In-app rewarded "ads" (currently simulated, not live) — no network activity

Payd includes an option to unlock temporary Pro access without paying. As of this document's last update, it is a 3-second in-app simulation of a rewarded ad: the paywall offers three unlock tiers, and tapping one shows a countdown card before the time is granted. The tiers grant 3, 12 and 24 hours, and stacked temporary Pro is capped at 24 hours from the moment of the grant, so repeated unlocks cannot substitute for subscribing (TEMP_PRO_TIER_HOURS and TEMP_PRO_CAP_HOURS in utils/entitlementRules.ts, covered by the unit tests in utils/__tests__/entitlements.test.ts). No ad is fetched or shown, no third-party ad network is contacted, no ad SDK is present in the app's dependencies, and this feature makes no network request at all — the countdown is a local timer. To be equally plain about intent: this is a planned feature, not a leftover. The reward flow is already built into the app, and integrating a real ad network is an intended future step — not a hypothetical. If a real ad network is added in a future version, this document will be updated before that version ships, and the specific network, the data it collects, and any consent flow (including Apple's App Tracking Transparency, currently not triggered because no ad SDK is present) will be disclosed here by name.

Everything else — bill entry, balance tracking, the cashflow chart, savings goals, tax estimates, PDF bank-statement parsing, portfolio screenshot import (Section 6.5), the Today screen, notifications, themes — runs entirely offline and makes zero network calls.


6. Third-Party Services, in Full

Payd integrates no third-party service that receives your data. Subscriptions go directly to Apple and Google (6.1), and the only optional data sources are the three stock-quote providers (6.2: Yahoo Finance, Finnhub, Alpha Vantage), which receive ticker symbols. There are no analytics SDKs, no crash-reporting SDKs, and no advertising SDKs in the app. This was verified by inspecting the app's full list of dependencies — there is no Sentry, Firebase, Amplitude, Mixpanel, Segment, or comparable service installed, and as of September 22, 2026 no subscription-billing service either. One planned exception deserves stating here, up front, rather than only in a subsection: ad-supported access is on Payd's roadmap (Section 5.3). No advertising SDK is present today, but when one is added, this document will name the network, describe exactly what data it collects, and document any consent flow — before that version ships, not after.

6.1 Subscriptions: Apple and Google directly, with no service in between

Payd Pro is an ordinary in-app subscription, sold and processed by the platform you bought your phone from. Payd asks the operating system's purchase API to start a purchase (requestPurchase) and asks it which subscriptions are currently active (getActiveSubscriptions); on iOS those come back as StoreKit 2 transactions signed by Apple and verified on device. The code lives in context/EntitlementsContext.tsx, and the rules that turn a store answer into "is this person Pro" are pure functions in utils/entitlementRules.ts, tested directly in utils/__tests__/entitlements.test.ts.

What this means for your data, stated as plainly as we can: no identifier is created for you anywhere in this path. There is no account, no anonymous user ID minted by Payd or by a billing vendor, and no API key in the app for a purchase service, because there is no purchase service. Apple or Google knows you bought a subscription — that is unavoidable when they are the ones taking the payment, and it is governed by their privacy policies, not ours. Nobody else learns anything. Payd stores only a local cache of whether Pro is active (payd_entitlements, Section 3); if you wipe it, Restore Purchases asks the store again.

Until September 22, 2026 this path ran through RevenueCat, a billing service that received purchase events tagged with a random anonymous ID it generated. That integration has been removed from the app entirely — the packages are uninstalled and no key for it remains in the project. The identifier it created is gone with it, which is why Known Limitation #3 of earlier versions ("RevenueCat's anonymous identifier persists after a local data reset") no longer appears in Section 10: it is not a limitation that has been reworded, it is one that stopped existing.

6.2 Yahoo Finance, Finnhub & Alpha Vantage (stock price lookups)

Described in Section 5.1, including the Investment Calculator's opt-in historical-returns fetch. These receive only a ticker symbol, on-demand — when you view the Portfolio tab, when you test an API key, or when you tick "Base on my holdings' past performance" in the Investment Calculator (Yahoo Finance and Alpha Vantage only for that request; Finnhub does not participate). If you optionally supply your own Finnhub or Alpha Vantage API key (used to raise your personal rate limit for lookups), that key is stored locally on your device and sent only to that provider, as part of your own request to their service.

6.3 Apple / Google push notification infrastructure — not used

Payd's bill and payday reminders are scheduled entirely on-device using each platform's local notification APIs. Payd never requests a remote push token from Apple or Google, and consequently never has one to send anywhere. This means Payd has no mechanism to send you a notification remotely, at all — every notification you receive was scheduled by your own device, in advance, based on data that never left it.

One consequence of scheduling locally deserves stating plainly, because it is a real exposure rather than a technicality: a scheduled reminder's text is held by the operating system, not by Payd, and the OS shows it wherever you have allowed notifications to appear — including on your lock screen, where it can be read without unlocking the device, and in your notification history afterwards. Payd cannot redact a notification the system is already holding.

Because of that, reminders leave the dollar figures out by default. With "Show amounts in reminders" off — which is how Payd ships (showAmountsInNotifications in types/index.ts, absent from the defaults, so off unless you switch it on) — a bill alert's body reads "Mark it paid in Payd", the payday check reads "Check your pay landed as expected.", a long-term bill reminder gives the due date and no figure, and a savings goal's reminder says only "Tap to log this payday's set-aside." The toggle sits in Settings beside "Hide amounts on widgets"; turning it on puts the exact amounts back into the reminder text, deliberately, because some people would rather see the number at a glance and accept that.

What the default does not hide, stated so you can judge it yourself:

On iOS there is no way for an app to force the system to hide a notification preview. "Show Previews" (Settings → Notifications → Payd) is your setting, and its default is "Always". Payd deliberately does not register a custom preview placeholder: when you do set previews to "When Unlocked" or "Never", iOS substitutes its own generic wording, which gives away less than an app-supplied label like "Bill reminder" would. That asymmetry is precisely why the amounts are left out of the body by default rather than relying on any platform flag. On Android there is something an app can do, and Payd now does it: both of its notification channels are created with lockscreenVisibility: PRIVATE, and every reminder is scheduled onto one of them, so Android itself redacts the content of Payd's notifications behind a secure lock screen. Your own Android lock-screen notification setting applies on top of that, as it does to any app. Turning Payd's reminders off in Settings removes the exposure entirely, and Reset All Data cancels and dismisses everything Payd has scheduled (Section 8.2).

6.4 Partner Sync (optional, user-initiated)

Partner Sync lets two people compare budgets without either party's phone talking to a server — because there is no server. When you tap "Share," Payd generates a snapshot of your data (listed exactly below) and hands it to the operating system's native share sheet. From there, you choose the channel — Messages, AirDrop, email, or anything else installed on your phone. Payd has no visibility into what happens after that; the transmission is entirely between your device and the app you picked.

The snapshot contains: your self-chosen display name, safe-to-spend amount, next payday date, total debt, your checking and savings totals, your unpaid bills — name, emoji, amount and due date, soonest first, up to ten — and your savings goals, up to ten, each with its name, emoji, target amount and the amount saved so far. It does not contain your exact take-home pay, which is deliberately left out of the payload, nor account numbers, institution names (beyond what you've manually typed as a bill or account label), or transaction-level history.

Important limitation, stated plainly: this snapshot is base64-encoded for compact transport, not encrypted. Anyone who intercepts the encoded string before it reaches your partner could decode and read it. In practice, its confidentiality depends entirely on the security of the channel you choose to send it through — iMessage and most modern messaging apps use their own end-to-end encryption for the message itself, which is what actually protects this data in transit, not anything Payd does to the payload. We recommend sharing it only through a channel you already trust with sensitive information.

6.5 Portfolio screenshot import (optional, on-device)

The Portfolio tab's bulk-add sheet can pre-fill holdings from a screenshot of your brokerage's positions screen, so you don't have to retype each ticker. Precisely what happens, in order:

  1. You pick an image using the operating system's own file/photo picker. This uses the system picker UI, which does not require Payd to hold a photo-library or camera permission — Payd receives only the single image you chose.
  2. The image is copied into Payd's private cache and read by Google ML Kit's on-device text recognition (@react-native-ml-kit/text-recognition, bundled model). Recognition runs entirely on your device; ML Kit's on-device APIs make no network request and the image is never uploaded anywhere.
  3. The recognized text is parsed locally into candidate tickers, share counts, and average costs, shown to you for review and editing. Nothing is saved until you explicitly tap "Add all," and then only the rows you kept — never the image or its raw text.
  4. The cached image copy is deleted the moment recognition finishes — success or failure — in the same code path (finally block in components/BulkAddSheet.tsx, checkable in source). Payd retains no copy, thumbnail, or OCR text.

ML Kit is an on-device library, not a service: it receives no identifier, sends nothing, and is subject to the same "no analytics, no tracking" verification as the rest of the dependency list (Section 6).

The same deletion contract covers the other file you can hand Payd. When you import a bank statement (PDF or CSV) for bill detection, the system picker copies it into Payd's private cache, it is parsed entirely on-device, and it is deleted in a finally block on every exit path — including when parsing fails or you back out of the review screen (app/statement-review.tsx). Only the bills you confirm are saved; the statement itself is never transmitted and never kept.


7. Permissions Payd Requests

Permission Platform Why
Notifications iOS & Android Local bill and payday reminders, scheduled on-device
Vibrate Android Haptic feedback on notification delivery
Receive boot completed Android Re-schedules local notifications after a device restart (notifications don't survive a reboot otherwise)
Request delete packages Android Lets the "Uninstall" button at the end of the reset flow open the operating system's own uninstall dialog (Section 8.3). It is a normal permission — no runtime prompt — and grants no ability to read anything; it only lets Payd ask the OS to start an uninstall that you then confirm
Face ID iOS Optional App Lock (off by default). Authentication is performed entirely by iOS; Payd receives only a success/failure result — never biometric data, never your passcode
Biometric / fingerprint Android Same as above, via Android's BiometricPrompt. These manifest entries are added by the authentication library; Payd stores no PIN of its own — the passcode fallback is your device's own lock screen

Two Android permissions that earlier builds requested are gone: SCHEDULE_EXACT_ALARM and USE_EXACT_ALARM, which Google Play restricts to alarm and calendar apps. Payd's reminders are now scheduled inexactly, so Android may deliver a bill nudge somewhat later than the minute it was set for.

The notification permission prompt appears the first time you switch a reminder on — from Settings → Notifications, when you create a savings goal with its payday reminder enabled or flip that reminder on later, or when you enable a long-term bill's set-aside reminder — and never at launch. Whatever you answer, the reminders themselves stay local: no push token exists, and nothing about them leaves the device (Section 6.3).

Payd explicitly blocks itself from requesting the following, even though some app templates include them by default: contacts (read/write) and call log. There is no feature in Payd that would use them, so the app declares them blocked at the manifest level rather than simply not asking.

Payd does not request: camera access (Partner Sync uses text paste via the share sheet, not a QR scanner), location, microphone, or contacts.


8. Data You Control

8.1 Local backup & restore

You can export a full backup of your Payd data as a file, saved via the OS share sheet — typically to Files/iCloud Drive on iOS or your Android file system, wherever you choose to save it. You control where that file lives; Payd has no involvement once it's saved. There are two formats: the .payd backup file under Backup & Restore, and the JSON export under Export, which is the one that can be passcode-encrypted.

Either file contains the Section 3 categories that make up your plan: everything in payd_data (pay schedule, bills, balances, spend log, budgets, income streams and events), your tax/paystub entries, portfolio holdings, savings goals, starting balances and your theme. It deliberately does not contain your subscription state (payd_entitlements): the file is plain text, so round-tripping that value would let anyone hand-edit themselves into Pro. RevenueCat remains the source of truth for what you've paid for, and Settings → Restore Purchases is the supported path.

You may optionally protect the JSON export with a passcode. When you do, Payd encrypts the file using AES-256 in CBC mode, with a key derived from your passcode via PBKDF2 with 10,000 iterations of SHA-256, and a freshly generated random salt and initialization vector on every export. There is no way to recover an encrypted backup if you forget the passcode — Payd never stores it, transmits it, or retains any way to reverse the derivation.

Both export paths have to write the file into Payd's own cache first, because the share sheet needs a real file to hand to the app you pick. That staging copy — which is plaintext unless you chose a passcode — is deleted as soon as the share sheet closes, on both paths, rather than being left in the cache afterwards (app/data.tsx). Because the app ships this encryption at all, its iOS build declares ITSAppUsesNonExemptEncryption: false in app.json: the standard assertion that it qualifies for the Category 5 Part 2 export exemption by using published, standard cryptography. That declaration states a fact about the binary; it changes nothing about what is encrypted.

8.2 Full local reset

Settings → Reset All Data permanently erases every category of data listed in Section 3, from every storage location Payd uses on your device — not a subset. This is implemented as a full clear of the app's local storage, not a list of individually-remembered keys, specifically so that no future feature can accidentally be left out of what "reset" means. Every other code path that resets data removes every payd_-prefixed key too, so the guarantee holds no matter where the reset starts from.

Before anything is cleared, the reset also cancels every reminder Payd has scheduled with the operating system and dismisses any already delivered. Without that step the bill names and amounts those notifications carry (Section 6.3) would survive the wipe on your lock screen and in your notification history — which would contradict what "erases everything" means.

There is no longer an exception to this. Earlier versions of this document disclosed one: a billing service's anonymous identifier that survived a reset and could only be cleared by deleting the app. That service was removed on September 22, 2026 (Section 6.1) and the identifier with it, so a local reset now clears everything Payd has put on your device, full stop.

One thing a reset does not and cannot touch, because it was never Payd's to hold: if you have an active subscription, that purchase still exists at Apple or Google. It is their record of a transaction you made with them, not a Payd identifier, and it is what lets Restore Purchases give you Pro back on a new phone. Cancelling a subscription is done in the store's own settings, which Payd links you to.

8.3 Deleting the app

After a full reset, Payd offers to help you remove the app itself. What actually happens differs by platform, and we describe both honestly rather than implying parity:


9. How This Compares Structurally to Bank-Linked Budgeting Apps

This is a factual, structural comparison — not a claim that one approach is universally better. Bank-linked apps offer real conveniences Payd does not: automatic transaction import, AI-assisted categorization, and a live view of exactly what cleared your account. Those features require your bank credentials or account data to pass through an aggregator (commonly Plaid, Yodlee, or Finicity) and then be stored by the app's own backend to power search, categorization, and sync across your devices.

Typical bank-linked app Payd
Requires bank login Yes, via an aggregator No
Stores transaction history on a server Yes No server exists
Auto-categorizes spending via AI Often Not currently a feature
Data breach exposes Aggregator + app's own database Nothing — no database exists
Works with no internet connection No (needs to sync/refresh) Yes, entirely
Cross-device sync Automatic, server-mediated Manual (Section 8.1) or Partner Sync (Section 6.4)

If your priority is minimizing how many parties ever see your bank credentials or transaction history, the structural difference above is real and verifiable — not a matter of trusting either company's stated policy, but a difference in what's technically possible to breach.


10. Known Limitations

Stated directly, without hedging:

  1. Local storage is not independently encrypted by Payd. It relies on your device's own lock-screen protection (Section 4). A jailbroken/rooted device, or one unlocked without a passcode, offers reduced protection — this is a real limitation we may address in a future version, and we will update this document when and if we do. The optional App Lock (Section 7) narrows one path to this data — someone holding your unlocked phone can no longer open Payd — but to be precise about what it is: App Lock gates the app's interface. It is not storage encryption, and it does not change what an attacker with filesystem-level access to a compromised device could read.
  2. Partner Sync payloads are not encrypted — only encoded for transport. Their confidentiality depends on the security of the channel you send them through (Section 6.4).
  3. True self-deletion is impossible on iOS by Apple's own design — Payd cannot remove itself from your device without your final manual tap (Section 8.3).
  4. The temporary-Pro unlock is currently a local simulation, not backed by a live ad network (Section 5.3) — and a real rewarded-ad integration is a planned feature, not a discarded experiment. When an ad network is integrated, it will collect data the rest of the app does not; the network and its data practices will be named here before any ad-enabled version ships.
  5. Locally scheduled reminders still carry your bill and goal names in text the operating system keeps. As of this version the dollar figures are left out of reminder bodies by default (Section 6.3), which narrows this limitation without closing it: the name and the due timing are still in the notification title, so "Rent due tomorrow" remains readable on a lock screen without unlocking the phone; the payday check still passes its expected-amount figure in the notification's undisplayed data payload; reminders scheduled before the change keep their original text until rescheduled; and anyone who turns "Show amounts in reminders" on puts exact amounts back into OS-held text. iOS gives an app no way to force a preview to be hidden — that is your own "Show Previews" setting — so on iPhone the protections are the default-off amounts, your own notification settings, and turning Payd's reminders off. On Android the system additionally redacts the notification's content behind a secure lock screen, because Payd schedules every reminder onto a channel marked private.

11. Security Practices We Follow

Payd has no backend server, no database, and no API endpoints of its own — which means the majority of a standard web-app security checklist (row-level database security, server-side input validation, rate limiting, CORS configuration, SQL injection prevention) does not apply, because there is no server-side surface for those risks to exist on. This isn't a loophole; it's the direct consequence of the architecture described in Section 1.

What we do apply, given that architecture:


12. Changelog

v2.0 — September 22, 2026 The third-party billing service is gone. This is a structural change to what the app can transmit, not a wording change:

  1. RevenueCat removed entirely (Sections 1, 3, 5.2, 6, 6.1, 8.2, 10, 11). react-native-purchases and react-native-purchases-ui are uninstalled and absent from package.json; no RevenueCat reference remains anywhere in the app's source. Subscriptions now go directly to StoreKit 2 on iOS and Google Play Billing on Android through expo-iap, with entitlement derived from the store's own active-subscription list and, on iOS, from Apple-signed transactions verified on device (context/EntitlementsContext.tsx, utils/entitlementRules.ts).
  2. No identifier is minted for this app any more. There was exactly one before — the anonymous ID RevenueCat generated. There is now no account, no anonymous user ID, and no logIn()-style call that could create one. Section 3's identifier discussion and Section 6.1 are rewritten accordingly.
  3. Known Limitation #3 removed — "RevenueCat's anonymous identifier persists after a local data reset, until the app itself is deleted" describes something that no longer exists. Every other limitation stands unchanged; the list renumbered, so the notification limitation that was #6 is now #5, and Section 8.2's "one documented exception" to a full reset is retired.
  4. No purchase secret remains in the project (Section 11). eas.json carries no RevenueCat keys in any profile, and .env.example documents that subscriptions need no key at all. The only keys anywhere are the optional quote-provider keys a user enters themselves.
  5. Network story simplified (Sections 1, 5). The only request class originating in Payd's own code is now a ticker-symbol quote lookup, and only if you use the Portfolio tab. Purchase traffic still exists but is the operating system's, made by Apple's or Google's store client on your behalf — Section 5 now says exactly that rather than counting "three things".
  6. Temporary Pro is capped (Section 5.3). The unlock tiers grant 3, 12 and 24 hours and stacked time cannot exceed 24 hours from the grant, so repeated unlocks cannot replace subscribing. The mechanic is otherwise unchanged and there is still no ad SDK.

v1.8 — September 17, 2026 A mitigation for the exposure disclosed in v1.7 earlier the same day, and the precise limits of that mitigation:

  1. Reminder bodies now omit every dollar figure by default (Sections 6.3, 10). A new Settings toggle, "Show amounts in reminders," ships off (showAmountsInNotifications, absent from DEFAULT_PREFERENCES in types/index.ts), and all four reminder types respect it: the payday check and bill alerts (context/AppContext.tsx), savings goal payday reminders (context/SavingsContext.tsx) and long-term bill set-aside reminders (components/LongTermBills.tsx). With it off, the bodies carry no amount at all — "Mark it paid in Payd", "Check your pay landed as expected." Turning it on restores the figures, by the user's own choice.
  2. Android now redacts the notification itself (Section 6.3). Payd's two notification channels were already created with lockscreenVisibility: PRIVATE, but nothing was routed to them — channelId is set on a notification's trigger, and no scheduling call set it, so Android delivered every reminder through a fallback channel with no visibility setting and the redaction never applied. All five scheduling sites now request a private channel explicitly, so on Android the content is hidden behind a secure lock screen until you unlock. iOS has no equivalent, which is why the omitted-amount default remains the primary protection.
  3. What that does not fix, kept in Known Limitation #6 rather than declared solved. Bill and goal names remain in notification titles with their due timing, so the title is still readable on a lock screen; the payday check still passes its expected-amount figure in the notification's undisplayed data payload, which the "Adjust amount" action uses; already-scheduled reminders keep their old text until rescheduled; and opting in restores exact amounts. Section 6.3 now lists each of these.
  4. The iOS constraint is stated plainly (Section 6.3). No app can force iOS to hide a notification preview — "Show Previews" is the user's setting and defaults to "Always" — and Payd deliberately registers no custom preview placeholder, because iOS's own generic substitution reveals less than an app-supplied label would. That is why the amount is removed from the body by default instead of relying on a platform flag.

v1.7 — September 17, 2026 One previously undisclosed exposure, several corrections, and a set of behavior changes disclosed before ship, per the living-document policy:

  1. Local reminders carry bill names and exact amounts, and the OS keeps that text (Sections 6.3, 8.2, 10). This was never documented and should have been. Bill alerts, the payday check, long-term bill reminders and savings payday reminders all put a name and a figure in the notification body; the operating system stores that content and shows it on the lock screen, where it can be read without unlocking the device. New Known Limitation #6 states this without hedging. At the time of this entry no in-app control to suppress the amounts existed; one shipped the same day — see v1.8 above. Reset All Data now cancels and dismisses every scheduled reminder before clearing storage, so a wipe no longer leaves that text on your lock screen.
  2. Partner Sync now shares savings goals (Section 6.4). The payload enumeration is meant to be exact, so this is the disclosure that matters most in this revision: a snapshot now also carries up to ten savings goals, each with its name, emoji, target amount and amount saved so far, and the receiving screen renders them as "Their savings goals". The enumeration was corrected at the same time — the bills it carries are your unpaid bills, soonest first, up to ten, not "recurring monthly bills only" — and your exact take-home pay is now explicitly excluded from the payload, which the enumeration never claimed to include.
  3. Portfolio quote timing corrected (Section 5.1). Earlier wording implied quotes were only fetched when you refreshed by hand. They are also fetched automatically if you turn on the Portfolio tab's "Auto" toggle (off by default) — now bounded to at most once per calendar day when the app opens or returns to the foreground, whether or not the attempt succeeds. The opt-in historical-returns fetch now also carries an 8-second per-request timeout and a 30-second ceiling for the whole run.
  4. Widget amounts are genuinely suppressed (Section 3). With "Hide amounts on widgets" on — the default — payd_widget_data now holds no money value at all: no safe-to-spend, balances, debt or net worth, and no per-bill amounts, only your next payday, days until it, and bill names and due dates.
  5. Backups no longer round-trip subscription state (Section 8.1). payd_entitlements is excluded from the export, so editing a backup file can no longer grant Pro. The section now also lists what a backup does contain, and names which of the two export formats the passcode encryption applies to.
  6. Plaintext staging copies are deleted (Section 8.1). Both export paths wrote the file into the app cache for the share sheet and left it there; each now deletes it once the share sheet closes. An imported bank statement is likewise deleted from the cache on every exit path (Section 6.5), matching the screenshot-import contract.
  7. Paystub figures no longer reach the device log (Section 11). A debug line in the Taxes screen printed a paystub's gross, net, federal, state and FICA amounts and hours into the OS log, where they outlive the app session. The line is gone. This is a correction, not a new feature: the "processed locally, never leaves the app" claim now holds for the device log too.
  8. Reset covers every key, from every path (Section 8.2). One reset path cleared a five-key subset, leaving API keys, App Lock state, review-prompt state, the historical-returns cache and daily checklist keys on disk. All paths now remove every payd_-prefixed key. Separately, daily checklist keys older than 7 days are pruned automatically, so that history no longer accumulates indefinitely (Section 3).
  9. Two more anonymous RevenueCat calls, and one storage key (Sections 3, 6.1). The SDK is now asked for localized store prices and free-trial eligibility at startup, and for subscription status once after a reset; all anonymous, no identifier, no new endpoint, logIn() still never called. payd_last_auto_contribution — one date, the last payday whose savings auto-contributions were applied — was added to Section 3's table.
  10. Android and iOS declarations changed (Sections 7, 8.1). SCHEDULE_EXACT_ALARM and USE_EXACT_ALARM were removed (Play restricts them to alarm and calendar apps; reminders now schedule inexactly) and REQUEST_DELETE_PACKAGES was added so the reset flow's Uninstall button can open the OS uninstall dialog — a normal permission with no runtime prompt that grants no read access. The iOS build now declares ITSAppUsesNonExemptEncryption: false, asserting the Category 5 Part 2 exemption for the backup encryption already described in Section 8.1. Section 7 also now says where the notification prompt can appear, which is no longer only Settings.
  11. Ad-simulation wording matches the shipped screens (Sections 5.3, 10). The paywall no longer says "ad" anywhere: it offers "Unlock 3 hours / 1 day / 3 days" under "Or try Pro free for a few hours", and the countdown card reads "UNLOCKING". The mechanic, the durations and the absence of any ad SDK are unchanged, and the disclosure that a real ad network is a planned future step stands.
  12. App Lock now covers screens that were already open. When the lock engages, the overlay is presented in its own window above the navigator (a full-window overlay on iOS, a modal on Android), so balances are no longer visible behind an open sheet or in the app-switcher snapshot.

v1.6 — July 19, 2026 New feature disclosed before ship, per the living-document policy:

  1. App Lock added (Sections 3, 7, 10, 11). An optional, off-by-default toggle in Settings → Privacy requires Face ID, fingerprint, or the device passcode to open Payd. Authentication is delegated entirely to the operating system — Payd receives only a success/failure result, never biometric data or the passcode, and stores no PIN of its own. The single new stored value is the on/off preference (payd_app_lock), cleared by Reset All Data like everything else. New OS permission entries: Face ID usage description (iOS) and biometric/fingerprint manifest entries (Android). If the device has no lock screen at all, App Lock fails open rather than locking you out of your own data, and cannot be enabled until one is set. Known Limitation #1 updated to state precisely what App Lock is (an interface gate) and is not (storage encryption). Not available on web builds.

v1.5 — July 19, 2026 New feature disclosed before this document's first publication:

  1. Investment Calculator's opt-in historical-returns projection added (Sections 3, 5.1, 6.2). An off-by-default checkbox — "Base on my holdings' past performance" — lets the calculator's Annual Return field be computed from your actual holdings' history instead of a manual guess. Ticking it fetches up to 5 years of monthly closing prices per ticker (Yahoo Finance, then Alpha Vantage if you've supplied a key), computes each holding's annualized return, and weights the result by position size. Same ticker-only request class as the existing price lookup in Section 5.1 — fires only when the box is ticked, never automatically, and results cache locally for 7 days (payd_hist_returns, added to Section 3's storage table). The calculator always displays the disclaimer: "Past performance does not guarantee future results — this is an estimate for planning, not investment advice."

v1.4 — July 13, 2026 Transparency correction, prompted by our own review — no code or data handling changed:

  1. Rewarded-ad intent disclosed up front. Previous versions described the rewarded-ad feature as "currently simulated" but left the planned nature of real ad integration easy to miss. Sections 5.3, 6, and 10 now state plainly that ad support is a planned feature with its reward flow already scaffolded in the app, and repeat the commitment that any ad network will be named here — with its data collection and consent flow — before an ad-enabled version ships. Marketing copy on this site was softened to match ("no analytics or tracking SDKs" rather than implying ads were never coming).

v1.3 — July 13, 2026 New feature disclosed before ship, per the living-document policy:

  1. Portfolio screenshot import added (Section 6.5). Holdings can be pre-filled from a brokerage screenshot using Google ML Kit's bundled, on-device text recognition. No network request is involved, the image is picked via the OS's own picker (no photo-library permission), and the cached copy is deleted immediately when recognition ends — the deletion lives in the same finally block as the recognition call, so no code path can skip it. A bulk manual-entry sheet (ticker, shares, average price) ships alongside it; both are Pro features inside the Portfolio tab.

v1.2 — July 8, 2026 Four additions/corrections, prompted by a full source re-audit (PRIVACY_AUDIT.md v2.0.0):

  1. API-key validation request disclosed. Testing a Finnhub or Alpha Vantage key in Settings makes one validation request to that provider, containing a fixed test ticker (AAPL) and the user's own key. Same data class as a quote lookup; previously undocumented. Section 5.1 updated.
  2. payd_api_keys added to the storage table (Section 3). User-entered provider API keys were already described as stored locally in Section 6.2, but the table omitted the row.
  3. Final Rork remnant removed. A leftover, inert build-config entry from the removed development platform (an expo-router origin pointing at the platform's domain) remained in app.json after the v1.1 cleanup. No request was ever made to it — the app contains no relative network calls that would use it — but it has now been deleted so the v1.1 "removed entirely" claim holds by construction. For clarity: identifiers such as payd_bill_*, payd_longterm_*, and payd_payday_check that appear in the app's source are names of locally scheduled notifications, not stored data, and so do not appear in Section 3's storage table.
  4. Eight unused packages removed from the dependency list. zod, zustand, expo-symbols, expo-image, expo-web-browser, expo-font, @expo/vector-icons, and react-native-worklets had zero references in Payd's source and were uninstalled. None had any network behavior, so nothing about the app's data handling changes — this simply shrinks the dependency surface that Section 6's "verified against the app's full list of dependencies" claim covers.

v1.1 — July 8, 2026 Two corrections, noted here per the policy at the top of this document:

  1. Alpha Vantage disclosed as a third stock-quote provider. The quote fallback chain has three providers (Yahoo Finance → Finnhub → Alpha Vantage), not two. Like the others, Alpha Vantage receives only a ticker symbol, and only when you've supplied your own free API key. Sections 5.1 and 6.2 updated.
  2. Development-platform analytics tooling removed from the build. The app was originally scaffolded on a development platform (Rork) whose build tooling automatically injected an analytics wrapper — backed by the PostHog SDK, pointed at the platform's own relay — into every build. It was dormant in Payd (the environment variables that activate it were never set, and no data was ever transmitted), but its presence in the compiled app contradicted this document's claim that no analytics SDK is bundled. That build tooling and the SDK it carried have now been removed entirely, along with unused server-framework and permission-bearing packages (expo-location, expo-image-picker) left over from the same template, so the claim is true by construction rather than by configuration.

v1.0 — July 8, 2026 Initial publication. Documents the local-first architecture, the three network-touching features (stock lookups, RevenueCat, simulated ad rewards), Partner Sync's unencrypted-transport limitation, AES-256 backup encryption, full-reset behavior, and platform-specific app deletion.


Questions or corrections

If you find anything in this document that doesn't match Payd's actual behavior, we want to know — please reach out so we can correct it. This document is meant to be checkable, not just readable.