Subscriptions
Getting paid
Overview
Subscriptions use RevenueCat. There are two products: monthly and yearly, both gated behind a single entitlement called "GetFolio Pro". The app checks this entitlement to decide what features to show. Free users get 1 portfolio, 3 alerts, and 3 assistant messages per day. Premium unlocks unlimited everything. The useSubscriptionStore manages all of this on the client side. In development mode, isPremium always returns true so we can test premium features without buying anything.
How it works
On app launch, the store calls Purchases.configure() with the RevenueCat API key. This initializes the SDK and sets up a customer info listener that reacts to any subscription changes in real time.
After Clerk auth completes, we call Purchases.logIn(userId) to link the RevenueCat customer to the Clerk user. This is what enables subscription portability across devices. If a user signs in on a new phone, their premium status transfers over.
The paywall screen calls fetchOfferings() to get the available packages from RevenueCat. When a user taps "Subscribe", purchasePackage() initiates the native App Store purchase flow. RevenueCat handles the receipt validation, and the customer info listener fires with the updated entitlements.
Feature gating happens at three levels. The useSubscriptionStore exposes isPremium. The usePremiumFeature() hook returns isPremium plus a requiresPremium() function that navigates to the paywall. And limits are checked in each store: usePortfolioStore checks the 1 portfolio limit, useAlertsStore checks the 3 alert limit, useAssistantStore checks the 3 message per day limit.
Restoration works through restorePurchases() which asks RevenueCat to check the App Store for any existing entitlements tied to the user's Apple ID. This covers cases where someone reinstalls the app or gets a new device.
The customer center (RevenueCat's pre built UI) handles plan changes, cancellations, and billing info. We link to it from the profile screen rather than building our own subscription management interface.
Key decisions
Entitlement based gating, not product based
We check for the "GetFolio Pro" entitlement, not for specific product IDs. This means we can change the product lineup (add a lifetime plan, adjust pricing, run promotions) without touching the app code. The entitlement is the abstraction layer between what the user bought and what features they get.
Free tier limits enforced on both client and server
The client checks limits before allowing actions (better UX, instant feedback). But the server also enforces them (prevents circumvention). For example, the alerts edge function checks the count server side before inserting. If a user somehow bypasses the client check, the server still blocks them. The client side check is for speed, the server side check is for security.
Dev mode always returns premium
During development, isPremium always returns true. This lets us test premium features without going through the purchase flow every time. The check is a simple __DEV__ ternary in the store. It means we never accidentally ship broken premium features because we could not test them locally.